From 731f206424f4cff6d9243fea97e89ab30949589d Mon Sep 17 00:00:00 2001 From: kernelkind Date: Wed, 25 Sep 2024 21:32:08 -0400 Subject: [PATCH 01/14] basic add column impl Signed-off-by: kernelkind --- src/nav.rs | 24 +++++++++++++++++++----- src/route.rs | 3 +++ src/timeline/route.rs | 14 ++++++++------ src/ui/add_column.rs | 43 +++++++++++++++++++++++++++++++++++++++++++ src/ui/mod.rs | 1 + src/ui/side_panel.rs | 7 +++++-- 6 files changed, 79 insertions(+), 13 deletions(-) create mode 100644 src/ui/add_column.rs diff --git a/src/nav.rs b/src/nav.rs index a0f3983..26409e9 100644 --- a/src/nav.rs +++ b/src/nav.rs @@ -3,8 +3,10 @@ use crate::{ relay_pool_manager::RelayPoolManager, route::Route, thread::thread_unsubscribe, - timeline::route::{render_timeline_route, TimelineRoute, TimelineRouteResponse}, - ui::{self, note::PostAction, RelayView, View}, + timeline::route::{render_timeline_route, AfterRouteExecution, TimelineRoute}, + ui::{ + self, add_column::{AddColumnResponse, AddColumnView}, note::PostAction, RelayView, View + }, Damus, }; @@ -73,12 +75,15 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { None } + Route::AddColumn => AddColumnView::new(app.accounts.get_selected_account()) + .ui(ui) + .map(AfterRouteExecution::AddColumn), }); - if let Some(reply_response) = nav_response.inner { + if let Some(after_route_execution) = nav_response.inner { // start returning when we're finished posting - match reply_response { - TimelineRouteResponse::Post(resp) => { + match after_route_execution { + AfterRouteExecution::Post(resp) => { if let Some(action) = resp.action { match action { PostAction::Post(_) => { @@ -87,6 +92,15 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { } } } + + AfterRouteExecution::AddColumn(add_column_resp) => { + match add_column_resp { + AddColumnResponse::Timeline(timeline) => { + app.columns_mut().add_timeline(timeline); + } + }; + app.columns_mut().column_mut(col).router_mut().go_back(); + } } } diff --git a/src/route.rs b/src/route.rs index f9c3736..2f46923 100644 --- a/src/route.rs +++ b/src/route.rs @@ -13,6 +13,7 @@ pub enum Route { Accounts(AccountsRoute), Relays, ComposeNote, + AddColumn, } impl Route { @@ -125,6 +126,8 @@ impl fmt::Display for Route { AccountsRoute::AddAccount => write!(f, "Add Account"), }, Route::ComposeNote => write!(f, "Compose Note"), + + Route::AddColumn => write!(f, "Add Column"), } } } diff --git a/src/timeline/route.rs b/src/timeline/route.rs index 941002e..1125b0d 100644 --- a/src/timeline/route.rs +++ b/src/timeline/route.rs @@ -8,6 +8,7 @@ use crate::{ timeline::TimelineId, ui::{ self, + add_column::AddColumnResponse, note::{ post::{PostAction, PostResponse}, QuoteRepostView, @@ -26,13 +27,14 @@ pub enum TimelineRoute { Quote(NoteId), } -pub enum TimelineRouteResponse { +pub enum AfterRouteExecution { Post(PostResponse), + AddColumn(AddColumnResponse), } -impl TimelineRouteResponse { +impl AfterRouteExecution { pub fn post(post: PostResponse) -> Self { - TimelineRouteResponse::Post(post) + AfterRouteExecution::Post(post) } } @@ -50,7 +52,7 @@ pub fn render_timeline_route( col: usize, textmode: bool, ui: &mut egui::Ui, -) -> Option { +) -> Option { match route { TimelineRoute::Timeline(timeline_id) => { if let Some(bar_action) = @@ -111,7 +113,7 @@ pub fn render_timeline_route( }); } - Some(TimelineRouteResponse::post(response.inner)) + Some(AfterRouteExecution::post(response.inner)) } TimelineRoute::Quote(id) => { @@ -140,7 +142,7 @@ pub fn render_timeline_route( np.to_quote(seckey, ¬e) }); } - Some(TimelineRouteResponse::post(response.inner)) + Some(AfterRouteExecution::post(response.inner)) } } } diff --git a/src/ui/add_column.rs b/src/ui/add_column.rs new file mode 100644 index 0000000..a174f95 --- /dev/null +++ b/src/ui/add_column.rs @@ -0,0 +1,43 @@ +use egui::{RichText, Ui}; +use nostrdb::FilterBuilder; + +use crate::{app_style::NotedeckTextStyle, timeline::Timeline, user_account::UserAccount}; + +pub enum AddColumnResponse { + Timeline(Timeline), +} + +pub struct AddColumnView<'a> { + cur_account: Option<&'a UserAccount>, +} + +impl<'a> AddColumnView<'a> { + pub fn new(cur_account: Option<&'a UserAccount>) -> Self { + Self { cur_account } + } + + pub fn ui(&mut self, ui: &mut Ui) -> Option { + ui.label(RichText::new("Add column").text_style(NotedeckTextStyle::Heading.text_style())); + + if ui.button("create global timeline").clicked() { + Some(AddColumnResponse::Timeline(create_global_timeline())) + } else { + None + } + } +} + +fn create_global_timeline() -> Timeline { + let filter = FilterBuilder::new().kinds([1]).build(); + Timeline::new( + crate::timeline::TimelineKind::Generic, + crate::filter::FilterState::Ready(vec![filter]), + ) +} + +// struct ColumnOption { +// title: &'static str, +// description: &'static str, +// icon: Box::, +// route: Route, +// } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index ac35e1c..08f239b 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,5 +1,6 @@ pub mod account_login_view; pub mod account_management; +pub mod add_column; pub mod anim; pub mod mention; pub mod note; diff --git a/src/ui/side_panel.rs b/src/ui/side_panel.rs index cf93ff1..b964059 100644 --- a/src/ui/side_panel.rs +++ b/src/ui/side_panel.rs @@ -186,8 +186,11 @@ impl<'a> DesktopSidePanel<'a> { } } SidePanelAction::Columns => { - // TODO - info!("Clicked columns button"); + if router.routes().iter().any(|&r| r == Route::AddColumn) { + router.go_back(); + } else { + router.route_to(Route::AddColumn); + } } SidePanelAction::ComposeNote => { if router.routes().iter().any(|&r| r == Route::ComposeNote) { From e2dd1b32980b9d75063f658d48f8366aca9909ca Mon Sep 17 00:00:00 2001 From: kernelkind Date: Wed, 25 Sep 2024 22:59:08 -0400 Subject: [PATCH 02/14] remote sub new timeline Signed-off-by: kernelkind --- src/app.rs | 124 ++++++++++++++++++++++++++++++++----------- src/nav.rs | 8 ++- src/ui/add_column.rs | 27 +++++----- 3 files changed, 112 insertions(+), 47 deletions(-) diff --git a/src/app.rs b/src/app.rs index 5e7b0af..c14b677 100644 --- a/src/app.rs +++ b/src/app.rs @@ -6,8 +6,7 @@ use crate::{ column::{Column, Columns}, draft::Drafts, error::{Error, FilterError}, - filter, - filter::FilterState, + filter::{self, FilterState}, frame_history::FrameHistory, imgcache::ImageCache, key_storage::KeyStorageType, @@ -17,7 +16,7 @@ use crate::{ route::Route, subscriptions::{SubKind, Subscriptions}, thread::Threads, - timeline::{Timeline, TimelineKind, ViewFilter}, + timeline::{Timeline, TimelineId, TimelineKind, ViewFilter}, ui::{self, DesktopSidePanel}, unknowns::UnknownIds, view_state::ViewState, @@ -41,6 +40,7 @@ use tracing::{debug, error, info, trace, warn}; pub enum DamusState { Initializing, Initialized, + NewTimelineSub(TimelineId), } /// We derive Deserialize/Serialize so we can persist app state on shutdown. @@ -394,44 +394,100 @@ fn setup_initial_nostrdb_subs( columns: &mut Columns, ) -> Result<()> { for timeline in columns.timelines_mut() { - match &timeline.filter { - FilterState::Ready(filters) => { - { setup_initial_timeline(ndb, timeline, note_cache, &filters.clone()) }? - } + setup_nostrdb_sub(ndb, note_cache, timeline)? + } - FilterState::Broken(err) => { - error!("FetchingRemote state broken in setup_initial_nostr_subs: {err}") - } - FilterState::FetchingRemote(_) => { - error!("FetchingRemote state in setup_initial_nostr_subs") - } - FilterState::GotRemote(_) => { - error!("GotRemote state in setup_initial_nostr_subs") - } - FilterState::NeedsRemote(_filters) => { - // can't do anything yet, we defer to first connect to send - // remote filters - } + Ok(()) +} + +fn setup_nostrdb_sub(ndb: &Ndb, note_cache: &mut NoteCache, timeline: &mut Timeline) -> Result<()> { + match &timeline.filter { + FilterState::Ready(filters) => { + { setup_initial_timeline(ndb, timeline, note_cache, &filters.clone()) }? + } + + FilterState::Broken(err) => { + error!("FetchingRemote state broken in setup_initial_nostr_subs: {err}") + } + FilterState::FetchingRemote(_) => { + error!("FetchingRemote state in setup_initial_nostr_subs") + } + FilterState::GotRemote(_) => { + error!("GotRemote state in setup_initial_nostr_subs") + } + FilterState::NeedsRemote(_filters) => { + // can't do anything yet, we defer to first connect to send + // remote filters } } Ok(()) } -fn update_damus(damus: &mut Damus, ctx: &egui::Context) { - if damus.state == DamusState::Initializing { - #[cfg(feature = "profiling")] - setup_profiling(); - - damus.state = DamusState::Initialized; - // this lets our eose handler know to close unknownids right away - damus - .subscriptions() - .insert("unknownids".to_string(), SubKind::OneShot); - setup_initial_nostrdb_subs(&damus.ndb, &mut damus.note_cache, &mut damus.columns) - .expect("home subscription failed"); +fn setup_new_nostrdb_sub( + ndb: &Ndb, + note_cache: &mut NoteCache, + columns: &mut Columns, + new_timeline_id: TimelineId, +) -> Result<()> { + if let Some(timeline) = columns.find_timeline_mut(new_timeline_id) { + info!("Setting up timeline sub for {}", timeline.id); + if let FilterState::Ready(filters) = &timeline.filter { + for filter in filters { + info!("Setting up filter {:?}", filter.json()); + } + } + setup_nostrdb_sub(ndb, note_cache, timeline)? } + Ok(()) +} + +fn update_damus(damus: &mut Damus, ctx: &egui::Context) { + match damus.state { + DamusState::Initializing => { + #[cfg(feature = "profiling")] + setup_profiling(); + + damus.state = DamusState::Initialized; + // this lets our eose handler know to close unknownids right away + damus + .subscriptions() + .insert("unknownids".to_string(), SubKind::OneShot); + setup_initial_nostrdb_subs(&damus.ndb, &mut damus.note_cache, &mut damus.columns) + .expect("home subscription failed"); + } + + DamusState::NewTimelineSub(new_timeline_id) => { + info!("adding new timeline {}", new_timeline_id); + setup_new_nostrdb_sub( + &damus.ndb, + &mut damus.note_cache, + &mut damus.columns, + new_timeline_id, + ) + .expect("new timeline subscription failed"); + + if let Some(filter) = { + let timeline = damus + .columns + .find_timeline(new_timeline_id) + .expect("timeline"); + match &timeline.filter { + FilterState::Ready(filters) => Some(filters.clone()), + _ => None, + } + } { + let subid = Uuid::new_v4().to_string(); + damus.pool.subscribe(subid, filter); + + damus.state = DamusState::Initialized; + } + } + + DamusState::Initialized => (), + }; + if let Err(err) = try_process_event(damus, ctx) { error!("error processing event: {}", err); } @@ -714,6 +770,10 @@ impl Damus { } } + pub fn add_new_timeline(&mut self, timeline_id: TimelineId) { + self.state = DamusState::NewTimelineSub(timeline_id); + } + pub fn mock>(data_path: P) -> Self { let mut columns = Columns::new(); let filter = Filter::from_json(include_str!("../queries/global.json")).unwrap(); diff --git a/src/nav.rs b/src/nav.rs index 26409e9..57e9404 100644 --- a/src/nav.rs +++ b/src/nav.rs @@ -5,7 +5,10 @@ use crate::{ thread::thread_unsubscribe, timeline::route::{render_timeline_route, AfterRouteExecution, TimelineRoute}, ui::{ - self, add_column::{AddColumnResponse, AddColumnView}, note::PostAction, RelayView, View + self, + add_column::{AddColumnResponse, AddColumnView}, + note::PostAction, + RelayView, View, }, Damus, }; @@ -75,7 +78,7 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { None } - Route::AddColumn => AddColumnView::new(app.accounts.get_selected_account()) + Route::AddColumn => AddColumnView::new(&app.ndb, app.accounts.get_selected_account()) .ui(ui) .map(AfterRouteExecution::AddColumn), }); @@ -96,6 +99,7 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { AfterRouteExecution::AddColumn(add_column_resp) => { match add_column_resp { AddColumnResponse::Timeline(timeline) => { + app.add_new_timeline(timeline.id); app.columns_mut().add_timeline(timeline); } }; diff --git a/src/ui/add_column.rs b/src/ui/add_column.rs index a174f95..dcfd9c4 100644 --- a/src/ui/add_column.rs +++ b/src/ui/add_column.rs @@ -1,40 +1,41 @@ use egui::{RichText, Ui}; -use nostrdb::FilterBuilder; +use nostrdb::Ndb; -use crate::{app_style::NotedeckTextStyle, timeline::Timeline, user_account::UserAccount}; +use crate::{ + app_style::NotedeckTextStyle, + timeline::{Timeline, TimelineKind}, + user_account::UserAccount, +}; pub enum AddColumnResponse { Timeline(Timeline), } pub struct AddColumnView<'a> { + ndb: &'a Ndb, cur_account: Option<&'a UserAccount>, } impl<'a> AddColumnView<'a> { - pub fn new(cur_account: Option<&'a UserAccount>) -> Self { - Self { cur_account } + pub fn new(ndb: &'a Ndb, cur_account: Option<&'a UserAccount>) -> Self { + Self { ndb, cur_account } } pub fn ui(&mut self, ui: &mut Ui) -> Option { ui.label(RichText::new("Add column").text_style(NotedeckTextStyle::Heading.text_style())); if ui.button("create global timeline").clicked() { - Some(AddColumnResponse::Timeline(create_global_timeline())) + Some(AddColumnResponse::Timeline( + TimelineKind::Universe + .into_timeline(self.ndb, None) + .expect("universe timeline"), + )) } else { None } } } -fn create_global_timeline() -> Timeline { - let filter = FilterBuilder::new().kinds([1]).build(); - Timeline::new( - crate::timeline::TimelineKind::Generic, - crate::filter::FilterState::Ready(vec![filter]), - ) -} - // struct ColumnOption { // title: &'static str, // description: &'static str, From 80be174f411d4ee43891f4e02e3343204734e7f4 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Thu, 26 Sep 2024 19:50:54 -0400 Subject: [PATCH 03/14] add more add column options Signed-off-by: kernelkind --- assets/icons/home_icon_dark_4x.png | Bin 0 -> 2847 bytes assets/icons/notifications_icon_dark_4x.png | Bin 0 -> 3403 bytes assets/icons/universe_icon_dark_4x.png | Bin 0 -> 3323 bytes src/ui/add_column.rs | 182 ++++++++++++++++++-- src/ui_preview/main.rs | 2 + 5 files changed, 167 insertions(+), 17 deletions(-) create mode 100644 assets/icons/home_icon_dark_4x.png create mode 100644 assets/icons/notifications_icon_dark_4x.png create mode 100644 assets/icons/universe_icon_dark_4x.png diff --git a/assets/icons/home_icon_dark_4x.png b/assets/icons/home_icon_dark_4x.png new file mode 100644 index 0000000000000000000000000000000000000000..56cf957806620f7e1e4342918edc8faca5cb7ffb GIT binary patch literal 2847 zcmV+)3*hvLP)@~0drDELIAGL9O(c600d`2O+f$vv5yP)j*g~Qty-1t>+4G`FE4*rsZ<8=7zbYG=Tgmnj$beFYh`|D9r&J) z*ZlJZJU>4_Um(p;UI4bSGyHG|8!2NOeSkuMe|MRGcY)6k!sA3DQD91>Fv|``k(C@KL<#_{olocZ$dDf7`N&EP5uya3q2cSW zy+W45!_OoH5dzTN@Qp|kLQe(28GVW~`dbte!#80jnM|JQWscAj0oYx+DSkLj1~AP+ z$gvR06fuzt0Pdb$;4K0DcTw0?$RklFijfHbt$t}~=^TH^pUBV>7Q$7|4Ng%+L?Qsh zdN%$e)9SqeZ@~3~@hEbG1ceM2bAII03>$x#3|<-F0jR^9Hf{R<;^N|eDMUgQz}VQ> z6+T_$V+|SnvW7ju-v$Q<>HYink0~TV2?2JzN1``Lh9E$9@kkiGgun%0_w9edLox(L zSb0I90x-#5xP&2szyx579}Pi^AP@l<<3~%7BIsHG#`wJZQogE7JC8YR7b32;xB2TG&ORonGn#o56*4G^mnvaf0dUrTm``R_kGf>+7O^zln(nx_R>^6^ljdxOeZK^_-@(^#U!EKo-C&-2|{@%N9C+{ygp4wX5mt zFJHc-si`S?^X3g{MxN^e?}lz6fkXPv#;xvFcmN((M!#SAIsIN-z8kuQ1TGQ(&R!s2`&_eL zK!y|tE+58+J8#F+r%$b;9h*0Ard%#Z$B!Q;4VY?tqAmn*=8rAdej!Ei>hjT|&zw0^ zH9oGtaN&Yw>~?4p66!_@>q5ZL(9l0fQTOlF<-_Plj~=aa=@7)jhYu|Q>E27=22P8M zi?h!6bpqh*|7TKgC9l^k|MKO_mWTNA<;#lKK7am9b8~ZulWjeS>b(SPrOTe~-|ybN z`=9l_^MZ;KK00;zZfxDUm9Aa8rgKQBhJmUFVE2!ju`=$z8<+3KhYufg4+$T+H!z0> zRROU3pXQSiyLbfQ^4-umB=Bb%W&z}^$M~X3@h2>o?}m;cf$crAu&_|FJOCGeGJN_w z2_{UJ?}m;cA(2S@#$KRc34q-{mUw+nLW#oVyP;u7c#S;3sRZsIfuO~q)gL-^sO@!V zacK2-?%biUxqkgR?ccxO`kU=IapHtE&j6o6C_mtf;{mRcu>OB9UA`NdgoHKh0WKvt z0Zdma6;WgV5?sC;8ia&38#ZjX-6yF4)T=DNTU6~Ouz2_Nb8T2e@yChxUdHa9Jb5C< z_yC5*dvXIp!Dp0`7qI6~^*i^{>oo@s9B^$xI)Kl3@Zdp`0BjEdd`ALs8@tzuwQJXs z1larkrC-Wgf4I@OASD^JJOH0X2;lDByVg;Qq&VLxjl!OtowW`LKmbYpGJr_}61a5f zQni2m;K2i{58dg6PAQ-`1tE`)j#}BjG*6e5w)n%#JUy?hU%%c;E+;1^eJ7YO6c3ir zjT<+*?zW-Z>Q*`I1+aaRGzBfr%f!%Dpv#5>9teGRh(SlX+9*;g2qBos8${0no&!RH zH_;I-1%N5sLDm!?B!m(j(NX|#-9dvqLPxX|0OEhp=vt*r+BYmrQ4H9#XOE}r?%%&p zaR9d0(gPU0!41ERQgDe3aixsGpx0!GCEWNw!UIHp$7+KNtQ^3EfcOI2_cJ9j#1i%Z zh5ke$Q6NJsv3ddaUa}?+h#zI_7v$6dN@R!|mgjc_5Zdj@4PvowFl7k<$p9H*1=~E2 zvn2pd1oLEw6;ASR2>^*8iUMSa3kBy!64k*V?h?qDs%i+G+K^v68aPA3Xf=^i;*#rD z?@8!n(CVJSRRJWENvJ|pJpe0ko1u~b1X8ZuD6%R5++vJfgQzgjYC1M%=V0Dn7&CwT zg7<>u?5$=DNm~cNHm>vZbz8+s+$+%QieZVs_}EQE5?B&mEEcOGsGAXFy9QBVK*}jLl;88WCumizs#MLkcegIP&e&5R*+fURINFV}h!0EUW}mzPZ_Pz2Zt zHuG|=YY1?_t;abO{FMaXS;SzGm?wvIzE>okOVGv#fo>Ix zH-_$ow34i8E;UJedm@V&6aP4N#u77!3VT^31-c~pJeeriQ4!eat~08 zof}+X<;QhIF^>VYC}MppdMW^iv9Ynkd^#7Su#o2?+lv<8hzJ2V#6Spn5@L7Wcb@1 xY>;$V9>R9(C4TrA8!mtS`Zd3>uuvj}`2({vcGC1*#jgMW002ovPDHLkV1hehN<;tv literal 0 HcmV?d00001 diff --git a/assets/icons/notifications_icon_dark_4x.png b/assets/icons/notifications_icon_dark_4x.png new file mode 100644 index 0000000000000000000000000000000000000000..cef65b7964835e831361958dbbc878e8d9e2515d GIT binary patch literal 3403 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 FV1i*03GDy? literal 0 HcmV?d00001 diff --git a/src/ui/add_column.rs b/src/ui/add_column.rs index dcfd9c4..3dde0ea 100644 --- a/src/ui/add_column.rs +++ b/src/ui/add_column.rs @@ -1,16 +1,40 @@ -use egui::{RichText, Ui}; +use egui::{vec2, ImageSource, Label, Layout, Margin, RichText, Sense, Ui}; use nostrdb::Ndb; use crate::{ app_style::NotedeckTextStyle, - timeline::{Timeline, TimelineKind}, + timeline::{PubkeySource, Timeline, TimelineKind}, user_account::UserAccount, }; +use super::padding; + pub enum AddColumnResponse { Timeline(Timeline), } +enum AddColumnOption { + Universe, + Notification(PubkeySource), + Home(PubkeySource), +} + +impl AddColumnOption { + pub fn take_as_response(self, ndb: &Ndb) -> Option { + match self { + AddColumnOption::Universe => TimelineKind::Universe + .into_timeline(ndb, None) + .map(AddColumnResponse::Timeline), + AddColumnOption::Notification(pubkey) => TimelineKind::Notifications(pubkey) + .into_timeline(ndb, None) + .map(AddColumnResponse::Timeline), + AddColumnOption::Home(pubkey) => TimelineKind::contact_list(pubkey) + .into_timeline(ndb, None) + .map(AddColumnResponse::Timeline), + } + } +} + pub struct AddColumnView<'a> { ndb: &'a Ndb, cur_account: Option<&'a UserAccount>, @@ -22,23 +46,147 @@ impl<'a> AddColumnView<'a> { } pub fn ui(&mut self, ui: &mut Ui) -> Option { - ui.label(RichText::new("Add column").text_style(NotedeckTextStyle::Heading.text_style())); + egui::Frame::none() + .outer_margin(Margin::symmetric(16.0, 20.0)) + .show(ui, |ui| { + ui.label( + RichText::new("Add column").text_style(NotedeckTextStyle::Body.text_style()), + ); + }); + ui.separator(); - if ui.button("create global timeline").clicked() { - Some(AddColumnResponse::Timeline( - TimelineKind::Universe - .into_timeline(self.ndb, None) - .expect("universe timeline"), - )) - } else { - None + let width_padding = 8.0; + let button_height = 69.0; + let icon_width = 32.0; + let mut selected_option: Option = None; + for column_option_data in self.get_column_options() { + let width = ui.available_width() - 2.0 * width_padding; + let (rect, resp) = ui.allocate_exact_size(vec2(width, button_height), Sense::click()); + ui.allocate_ui_at_rect(rect, |ui| { + padding(Margin::symmetric(width_padding, 0.0), ui, |ui| { + ui.allocate_ui_with_layout( + vec2(width, button_height), + Layout::left_to_right(egui::Align::Center), + |ui| { + self.column_option_ui( + ui, + icon_width, + column_option_data.icon, + column_option_data.title, + column_option_data.description, + ); + }, + ) + .response + }) + }); + ui.separator(); + + if resp.clicked() { + if let Some(resp) = column_option_data.option.take_as_response(self.ndb) { + selected_option = Some(resp); + } + } } + + selected_option + } + + fn column_option_ui( + &mut self, + ui: &mut Ui, + icon_width: f32, + icon: ImageSource<'_>, + title: &str, + description: &str, + ) { + ui.add(egui::Image::new(icon).fit_to_exact_size(vec2(icon_width, icon_width))); + + ui.vertical(|ui| { + ui.add_space(16.0); + ui.add( + Label::new(RichText::new(title).text_style(NotedeckTextStyle::Body.text_style())) + .selectable(false), + ); + + ui.add( + Label::new( + RichText::new(description).text_style(NotedeckTextStyle::Button.text_style()), + ) + .selectable(false), + ); + }); + } + + fn get_column_options(&self) -> Vec { + let mut vec = Vec::new(); + vec.push(ColumnOptionData { + title: "Universe", + description: "See the whole nostr universe", + icon: egui::include_image!("../../assets/icons/universe_icon_dark_4x.png"), + option: AddColumnOption::Universe, + }); + + if let Some(acc) = self.cur_account { + let source = PubkeySource::Explicit(acc.pubkey); + + vec.push(ColumnOptionData { + title: "Home timeline", + description: "See recommended notes first", + icon: egui::include_image!("../../assets/icons/home_icon_dark_4x.png"), + option: AddColumnOption::Home(source.clone()), + }); + vec.push(ColumnOptionData { + title: "Notifications", + description: "Stay up to date with notifications and mentions", + icon: egui::include_image!("../../assets/icons/notifications_icon_dark_4x.png"), + option: AddColumnOption::Notification(source), + }); + } + + vec } } -// struct ColumnOption { -// title: &'static str, -// description: &'static str, -// icon: Box::, -// route: Route, -// } +struct ColumnOptionData { + title: &'static str, + description: &'static str, + icon: ImageSource<'static>, + option: AddColumnOption, +} + +mod preview { + use crate::{ + test_data, + ui::{Preview, PreviewConfig, View}, + Damus, + }; + + use super::AddColumnView; + + pub struct AddColumnPreview { + app: Damus, + } + + impl AddColumnPreview { + fn new() -> Self { + let app = test_data::test_app(); + + AddColumnPreview { app } + } + } + + impl View for AddColumnPreview { + fn ui(&mut self, ui: &mut egui::Ui) { + AddColumnView::new(&self.app.ndb, self.app.accounts.get_selected_account()).ui(ui); + } + } + + impl<'a> Preview for AddColumnView<'a> { + type Prev = AddColumnPreview; + + fn preview(_cfg: PreviewConfig) -> Self::Prev { + AddColumnPreview::new() + } + } +} diff --git a/src/ui_preview/main.rs b/src/ui_preview/main.rs index a9804a6..9a0d6f9 100644 --- a/src/ui_preview/main.rs +++ b/src/ui_preview/main.rs @@ -1,6 +1,7 @@ use notedeck::app_creation::{ generate_mobile_emulator_native_options, generate_native_options, setup_cc, }; +use notedeck::ui::add_column::AddColumnView; use notedeck::ui::{ account_login_view::AccountLoginView, account_management::AccountsView, DesktopSidePanel, PostView, Preview, PreviewApp, PreviewConfig, ProfilePic, ProfilePreview, RelayView, @@ -102,5 +103,6 @@ async fn main() { AccountsView, DesktopSidePanel, PostView, + AddColumnView, ); } From ebe4bf30467d364e08b7bca6d5e3f3f26cc32de6 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Fri, 27 Sep 2024 18:53:55 -0400 Subject: [PATCH 04/14] animate add column options Signed-off-by: kernelkind --- src/app_style.rs | 13 +++- src/ui/add_column.rs | 168 +++++++++++++++++++++++++++---------------- 2 files changed, 117 insertions(+), 64 deletions(-) diff --git a/src/app_style.rs b/src/app_style.rs index 71a0b7d..6e9faf6 100644 --- a/src/app_style.rs +++ b/src/app_style.rs @@ -1,5 +1,6 @@ -use crate::colors::{ - desktop_dark_color_theme, light_color_theme, mobile_dark_color_theme, ColorTheme, +use crate::{ + colors::{desktop_dark_color_theme, light_color_theme, mobile_dark_color_theme, ColorTheme}, + ui::is_narrow, }; use egui::{ epaint::Shadow, @@ -96,6 +97,14 @@ pub fn mobile_font_size(text_style: &NotedeckTextStyle) -> f32 { } } +pub fn get_font_size(ctx: &egui::Context, text_style: &NotedeckTextStyle) -> f32 { + if is_narrow(ctx) { + mobile_font_size(text_style) + } else { + desktop_font_size(text_style) + } +} + #[derive(Copy, Clone, Eq, PartialEq, Debug, EnumIter)] pub enum NotedeckTextStyle { Heading, diff --git a/src/ui/add_column.rs b/src/ui/add_column.rs index 3dde0ea..c75e3c4 100644 --- a/src/ui/add_column.rs +++ b/src/ui/add_column.rs @@ -1,18 +1,20 @@ -use egui::{vec2, ImageSource, Label, Layout, Margin, RichText, Sense, Ui}; +use egui::{pos2, vec2, Color32, FontId, ImageSource, Pos2, Rect, Separator, Ui}; use nostrdb::Ndb; use crate::{ - app_style::NotedeckTextStyle, + app_style::{get_font_size, NotedeckTextStyle}, timeline::{PubkeySource, Timeline, TimelineKind}, + ui::anim::ICON_EXPANSION_MULTIPLE, user_account::UserAccount, }; -use super::padding; +use super::anim::AnimationHelper; pub enum AddColumnResponse { Timeline(Timeline), } +#[derive(Clone)] enum AddColumnOption { Universe, Notification(PubkeySource), @@ -46,76 +48,118 @@ impl<'a> AddColumnView<'a> { } pub fn ui(&mut self, ui: &mut Ui) -> Option { - egui::Frame::none() - .outer_margin(Margin::symmetric(16.0, 20.0)) - .show(ui, |ui| { - ui.label( - RichText::new("Add column").text_style(NotedeckTextStyle::Body.text_style()), - ); - }); - ui.separator(); - - let width_padding = 8.0; - let button_height = 69.0; - let icon_width = 32.0; let mut selected_option: Option = None; for column_option_data in self.get_column_options() { - let width = ui.available_width() - 2.0 * width_padding; - let (rect, resp) = ui.allocate_exact_size(vec2(width, button_height), Sense::click()); - ui.allocate_ui_at_rect(rect, |ui| { - padding(Margin::symmetric(width_padding, 0.0), ui, |ui| { - ui.allocate_ui_with_layout( - vec2(width, button_height), - Layout::left_to_right(egui::Align::Center), - |ui| { - self.column_option_ui( - ui, - icon_width, - column_option_data.icon, - column_option_data.title, - column_option_data.description, - ); - }, - ) - .response - }) - }); - ui.separator(); - - if resp.clicked() { - if let Some(resp) = column_option_data.option.take_as_response(self.ndb) { - selected_option = Some(resp); - } + let option = column_option_data.option.clone(); + if self.column_option_ui(ui, column_option_data).clicked() { + selected_option = option.take_as_response(self.ndb); } + + ui.add(Separator::default().spacing(0.0)); } selected_option } - fn column_option_ui( - &mut self, - ui: &mut Ui, - icon_width: f32, - icon: ImageSource<'_>, - title: &str, - description: &str, - ) { - ui.add(egui::Image::new(icon).fit_to_exact_size(vec2(icon_width, icon_width))); + fn column_option_ui(&mut self, ui: &mut Ui, data: ColumnOptionData) -> egui::Response { + let icon_padding = 8.0; + let min_icon_width = 32.0; + let height_padding = 12.0; + let max_width = ui.available_width(); + let title_style = NotedeckTextStyle::Body; + let desc_style = NotedeckTextStyle::Button; + let title_min_font_size = get_font_size(ui.ctx(), &title_style); + let desc_min_font_size = get_font_size(ui.ctx(), &desc_style); - ui.vertical(|ui| { - ui.add_space(16.0); - ui.add( - Label::new(RichText::new(title).text_style(NotedeckTextStyle::Body.text_style())) - .selectable(false), + let max_height = { + let max_wrap_width = + max_width - ((icon_padding * 2.0) + (min_icon_width * ICON_EXPANSION_MULTIPLE)); + let title_max_font = FontId::new( + title_min_font_size * ICON_EXPANSION_MULTIPLE, + title_style.font_family(), ); - - ui.add( - Label::new( - RichText::new(description).text_style(NotedeckTextStyle::Button.text_style()), + let desc_max_font = FontId::new( + desc_min_font_size * ICON_EXPANSION_MULTIPLE, + desc_style.font_family(), + ); + let max_desc_galley = ui.fonts(|f| { + f.layout( + data.description.to_string(), + desc_max_font, + Color32::WHITE, + max_wrap_width, ) - .selectable(false), - ); - }); + }); + + let max_title_galley = ui.fonts(|f| { + f.layout( + data.title.to_string(), + title_max_font, + Color32::WHITE, + max_wrap_width, + ) + }); + + let desc_font_max_size = max_desc_galley.rect.height(); + let title_font_max_size = max_title_galley.rect.height(); + title_font_max_size + desc_font_max_size + (2.0 * height_padding) + }; + + let helper = AnimationHelper::new(ui, data.title, vec2(max_width, max_height)); + let animation_rect = helper.get_animation_rect(); + + let cur_icon_width = helper.scale_1d_pos(min_icon_width); + let painter = ui.painter_at(animation_rect); + + let cur_icon_size = vec2(cur_icon_width, cur_icon_width); + let cur_icon_x_pos = animation_rect.left() + (icon_padding) + (cur_icon_width / 2.0); + + let title_cur_font = FontId::new( + helper.scale_1d_pos(title_min_font_size), + title_style.font_family(), + ); + + let desc_cur_font = FontId::new( + helper.scale_1d_pos(desc_min_font_size), + desc_style.font_family(), + ); + + let wrap_width = max_width - (cur_icon_width + (icon_padding * 2.0)); + let text_color = ui.ctx().style().visuals.text_color(); + let fallback_color = ui.ctx().style().visuals.weak_text_color(); + + let title_galley = painter.layout( + data.title.to_string(), + title_cur_font, + text_color, + wrap_width, + ); + let desc_galley = painter.layout( + data.description.to_string(), + desc_cur_font, + text_color, + wrap_width, + ); + + let galley_heights = title_galley.rect.height() + desc_galley.rect.height(); + + let cur_height_padding = (animation_rect.height() - galley_heights) / 2.0; + let corner_x_pos = cur_icon_x_pos + (cur_icon_width / 2.0) + icon_padding; + let title_corner_pos = Pos2::new(corner_x_pos, animation_rect.top() + cur_height_padding); + let desc_corner_pos = Pos2::new( + corner_x_pos, + title_corner_pos.y + title_galley.rect.height(), + ); + + let icon_cur_y = animation_rect.top() + cur_height_padding + (galley_heights / 2.0); + let icon_img = egui::Image::new(data.icon).fit_to_exact_size(cur_icon_size); + let icon_rect = Rect::from_center_size(pos2(cur_icon_x_pos, icon_cur_y), cur_icon_size); + + icon_img.paint_at(ui, icon_rect); + painter.galley(title_corner_pos, title_galley, fallback_color); + painter.galley(desc_corner_pos, desc_galley, fallback_color); + + helper.take_animation_response() } fn get_column_options(&self) -> Vec { From 57069ff7c0d0e6918d107f724c169148f54c6ef6 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Tue, 1 Oct 2024 12:51:14 -0400 Subject: [PATCH 05/14] push column picker immediately to new column instead of pushing to temporary column first Signed-off-by: kernelkind --- src/app.rs | 27 ++++----------------------- src/column.rs | 16 ++++++++++++++++ src/nav.rs | 34 ++++++++++++++++++++-------------- src/route.rs | 24 ++++++++++++++++++++++++ src/timeline/route.rs | 2 -- src/ui/side_panel.rs | 23 ++++++++++++++++------- 6 files changed, 80 insertions(+), 46 deletions(-) diff --git a/src/app.rs b/src/app.rs index c14b677..0753a96 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,7 +3,7 @@ use crate::{ app_creation::setup_cc, app_style::user_requested_visuals_change, args::Args, - column::{Column, Columns}, + column::Columns, draft::Drafts, error::{Error, FilterError}, filter::{self, FilterState}, @@ -13,7 +13,6 @@ use crate::{ nav, note::NoteRef, notecache::{CachedNote, NoteCache}, - route::Route, subscriptions::{SubKind, Subscriptions}, thread::Threads, timeline::{Timeline, TimelineId, TimelineKind, ViewFilter}, @@ -699,11 +698,7 @@ impl Damus { let debug = parsed_args.debug; if columns.columns().is_empty() { - let filter = Filter::from_json(include_str!("../queries/timeline.json")).unwrap(); - columns.add_timeline(Timeline::new( - TimelineKind::Generic, - FilterState::ready(vec![filter]), - )) + columns.new_column_picker(); } Self { @@ -770,7 +765,7 @@ impl Damus { } } - pub fn add_new_timeline(&mut self, timeline_id: TimelineId) { + pub fn subscribe_new_timeline(&mut self, timeline_id: TimelineId) { self.state = DamusState::NewTimelineSub(timeline_id); } @@ -993,22 +988,8 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, columns: usiz ) .show(ui); - let router = if let Some(router) = app - .columns - .columns_mut() - .get_mut(0) - .map(|c: &mut Column| c.router_mut()) - { - router - } else { - // TODO(jb55): Maybe we should have an empty column route? - let columns = app.columns.columns_mut(); - columns.push(Column::new(vec![Route::accounts()])); - columns[0].router_mut() - }; - if side_panel.response.clicked() { - DesktopSidePanel::perform_action(router, side_panel.action); + DesktopSidePanel::perform_action(app.columns_mut(), side_panel.action); } // vertical sidebar line diff --git a/src/column.rs b/src/column.rs index 16e4d5b..bfc7a83 100644 --- a/src/column.rs +++ b/src/column.rs @@ -47,6 +47,22 @@ impl Columns { self.columns.push(Column::new(routes)) } + pub fn add_timeline_to_column(&mut self, col: usize, timeline: Timeline) -> bool { + if let Some(column) = self.columns.get_mut(col) { + column + .router_mut() + .route_to_replaced(Route::timeline(timeline.id)); + self.timelines.push(timeline); + true + } else { + false + } + } + + pub fn new_column_picker(&mut self) { + self.columns.push(Column::new(vec![Route::AddColumn])); + } + pub fn columns_mut(&mut self) -> &mut Vec { &mut self.columns } diff --git a/src/nav.rs b/src/nav.rs index 57e9404..0702efb 100644 --- a/src/nav.rs +++ b/src/nav.rs @@ -78,9 +78,21 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { None } - Route::AddColumn => AddColumnView::new(&app.ndb, app.accounts.get_selected_account()) - .ui(ui) - .map(AfterRouteExecution::AddColumn), + Route::AddColumn => { + let resp = AddColumnView::new(&app.ndb, app.accounts.get_selected_account()).ui(ui); + + if let Some(resp) = resp { + match resp { + AddColumnResponse::Timeline(timeline) => { + let id = timeline.id; + if app.columns_mut().add_timeline_to_column(col, timeline) { + app.subscribe_new_timeline(id); + } + } + }; + } + None + } }); if let Some(after_route_execution) = nav_response.inner { @@ -95,16 +107,6 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { } } } - - AfterRouteExecution::AddColumn(add_column_resp) => { - match add_column_resp { - AddColumnResponse::Timeline(timeline) => { - app.add_new_timeline(timeline.id); - app.columns_mut().add_timeline(timeline); - } - }; - app.columns_mut().column_mut(col).router_mut().go_back(); - } } } @@ -120,6 +122,10 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { ); } } else if let Some(NavAction::Navigated) = nav_response.action { - app.columns_mut().column_mut(col).router_mut().navigating = false; + let cur_router = app.columns_mut().column_mut(col).router_mut(); + cur_router.navigating = false; + if cur_router.is_replacing() { + cur_router.remove_previous_route(); + } } } diff --git a/src/route.rs b/src/route.rs index 2f46923..22828ab 100644 --- a/src/route.rs +++ b/src/route.rs @@ -61,6 +61,7 @@ pub struct Router { routes: Vec, pub returning: bool, pub navigating: bool, + replacing: bool, } impl Router { @@ -70,10 +71,12 @@ impl Router { } let returning = false; let navigating = false; + let replacing = false; Router { routes, returning, navigating, + replacing, } } @@ -82,6 +85,13 @@ impl Router { self.routes.push(route); } + // Route to R. Then when it is successfully placed, should call `remove_previous_route` + pub fn route_to_replaced(&mut self, route: R) { + self.navigating = true; + self.replacing = true; + self.routes.push(route); + } + /// Go back, start the returning process pub fn go_back(&mut self) -> Option { if self.returning || self.routes.len() == 1 { @@ -100,6 +110,20 @@ impl Router { self.routes.pop() } + pub fn remove_previous_route(&mut self) -> Option { + let num_routes = self.routes.len(); + if num_routes <= 1 { + return None; + } + self.returning = false; + self.replacing = false; + Some(self.routes.remove(num_routes - 2)) + } + + pub fn is_replacing(&self) -> bool { + self.replacing + } + pub fn top(&self) -> &R { self.routes.last().expect("routes can't be empty") } diff --git a/src/timeline/route.rs b/src/timeline/route.rs index 1125b0d..71b9774 100644 --- a/src/timeline/route.rs +++ b/src/timeline/route.rs @@ -8,7 +8,6 @@ use crate::{ timeline::TimelineId, ui::{ self, - add_column::AddColumnResponse, note::{ post::{PostAction, PostResponse}, QuoteRepostView, @@ -29,7 +28,6 @@ pub enum TimelineRoute { pub enum AfterRouteExecution { Post(PostResponse), - AddColumn(AddColumnResponse), } impl AfterRouteExecution { diff --git a/src/ui/side_panel.rs b/src/ui/side_panel.rs index b964059..b2d1097 100644 --- a/src/ui/side_panel.rs +++ b/src/ui/side_panel.rs @@ -4,7 +4,7 @@ use tracing::info; use crate::{ account_manager::AccountsRoute, colors, - column::Column, + column::{Column, Columns}, imgcache::ImageCache, route::{Route, Router}, user_account::UserAccount, @@ -162,7 +162,8 @@ impl<'a> DesktopSidePanel<'a> { helper.take_animation_response() } - pub fn perform_action(router: &mut Router, action: SidePanelAction) { + pub fn perform_action(columns: &mut Columns, action: SidePanelAction) { + let router = get_first_router(columns); match action { SidePanelAction::Panel => {} // TODO SidePanelAction::Account => { @@ -189,7 +190,7 @@ impl<'a> DesktopSidePanel<'a> { if router.routes().iter().any(|&r| r == Route::AddColumn) { router.go_back(); } else { - router.route_to(Route::AddColumn); + columns.new_column_picker(); } } SidePanelAction::ComposeNote => { @@ -233,6 +234,17 @@ fn settings_button(dark_mode: bool) -> impl Widget { } } +fn get_first_router(columns: &mut Columns) -> &mut Router { + if columns.columns().is_empty() { + columns.new_column_picker(); + } + columns + .columns_mut() + .get_mut(0) + .expect("There should be at least one column") + .router_mut() +} + fn add_column_button(dark_mode: bool) -> impl Widget { let _ = dark_mode; move |ui: &mut egui::Ui| { @@ -391,10 +403,7 @@ mod preview { ); let response = panel.show(ui); - DesktopSidePanel::perform_action( - self.app.columns.columns_mut()[0].router_mut(), - response.action, - ); + DesktopSidePanel::perform_action(&mut self.app.columns, response.action); }); }); } From 45223dbc250771a706f10befe645409e194426df Mon Sep 17 00:00:00 2001 From: kernelkind Date: Tue, 1 Oct 2024 12:59:00 -0400 Subject: [PATCH 06/14] move get first router to Columns Signed-off-by: kernelkind --- src/column.rs | 12 ++++++++++++ src/ui/side_panel.rs | 15 ++------------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/column.rs b/src/column.rs index bfc7a83..b5f971c 100644 --- a/src/column.rs +++ b/src/column.rs @@ -63,6 +63,18 @@ impl Columns { self.columns.push(Column::new(vec![Route::AddColumn])); } + // Get the first router in the columns if there are columns present. + // Otherwise, create a new column picker and return the router + pub fn get_first_router(&mut self) -> &mut Router { + if self.columns.is_empty() { + self.new_column_picker(); + } + self.columns + .get_mut(0) + .expect("There should be at least one column") + .router_mut() + } + pub fn columns_mut(&mut self) -> &mut Vec { &mut self.columns } diff --git a/src/ui/side_panel.rs b/src/ui/side_panel.rs index b2d1097..e676222 100644 --- a/src/ui/side_panel.rs +++ b/src/ui/side_panel.rs @@ -6,7 +6,7 @@ use crate::{ colors, column::{Column, Columns}, imgcache::ImageCache, - route::{Route, Router}, + route::Route, user_account::UserAccount, Damus, }; @@ -163,7 +163,7 @@ impl<'a> DesktopSidePanel<'a> { } pub fn perform_action(columns: &mut Columns, action: SidePanelAction) { - let router = get_first_router(columns); + let router = columns.get_first_router(); match action { SidePanelAction::Panel => {} // TODO SidePanelAction::Account => { @@ -234,17 +234,6 @@ fn settings_button(dark_mode: bool) -> impl Widget { } } -fn get_first_router(columns: &mut Columns) -> &mut Router { - if columns.columns().is_empty() { - columns.new_column_picker(); - } - columns - .columns_mut() - .get_mut(0) - .expect("There should be at least one column") - .router_mut() -} - fn add_column_button(dark_mode: bool) -> impl Widget { let _ = dark_mode; move |ui: &mut egui::Ui| { From 0a22210c89fcac30c1f5df347a2b3c5b4d81a128 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Wed, 2 Oct 2024 17:11:30 -0400 Subject: [PATCH 07/14] tmp use kernelkind egui-nav Signed-off-by: kernelkind --- Cargo.lock | 17 ++++++++++++----- Cargo.toml | 3 ++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ac55f09..246264b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1148,7 +1148,7 @@ dependencies = [ [[package]] name = "egui_nav" version = "0.1.0" -source = "git+https://github.com/damus-io/egui-nav?rev=b19742503329a13df660ac8c5a3ada4a25b7cc53#b19742503329a13df660ac8c5a3ada4a25b7cc53" +source = "git+https://github.com/kernelkind/egui-nav?rev=6a7b1cf735e8a770b2510fbac06837d47c44e2c0#6a7b1cf735e8a770b2510fbac06837d47c44e2c0" dependencies = [ "egui", "egui_extras", @@ -1660,7 +1660,7 @@ checksum = "9c08c1f623a8d0b722b8b99f821eb0ba672a1618f0d3b16ddbee1cedd2dd8557" dependencies = [ "bitflags 2.6.0", "gpu-descriptor-types", - "hashbrown", + "hashbrown 0.14.5", ] [[package]] @@ -1692,6 +1692,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + [[package]] name = "hassle-rs" version = "0.11.0" @@ -1950,12 +1956,12 @@ checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" [[package]] name = "indexmap" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.0", "serde", ] @@ -2508,6 +2514,7 @@ dependencies = [ "env_logger 0.10.2", "hex", "image", + "indexmap", "log", "nostrdb", "poll-promise", diff --git a/Cargo.toml b/Cargo.toml index 75404f1..2f1836b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ eframe = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da egui_extras = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "egui_extras", features = ["all_loaders"] } ehttp = "0.2.0" egui_tabs = { git = "https://github.com/damus-io/egui-tabs", branch = "egui-0.28" } -egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "b19742503329a13df660ac8c5a3ada4a25b7cc53" } +egui_nav = { git = "https://github.com/kernelkind/egui-nav", rev = "6a7b1cf735e8a770b2510fbac06837d47c44e2c0" } egui_virtual_list = { git = "https://github.com/jb55/hello_egui", branch = "egui-0.28", package = "egui_virtual_list" } reqwest = { version = "0.12.4", default-features = false, features = [ "rustls-tls-native-roots" ] } image = { version = "0.25", features = ["jpeg", "png", "webp"] } @@ -42,6 +42,7 @@ strum = "0.26" strum_macros = "0.26" bitflags = "2.5.0" uuid = { version = "1.10.0", features = ["v4"] } +indexmap = "2.6.0" [target.'cfg(target_os = "macos")'.dependencies] From 1bf9d5d934c9d5b0cf47ea68c1eee82ccfb96e83 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Wed, 2 Oct 2024 19:39:05 -0400 Subject: [PATCH 08/14] title bar add title bar to columns with title specific to the column type. also add column deletion button Signed-off-by: kernelkind --- assets/icons/column_delete_icon_4x.png | Bin 0 -> 806 bytes src/app.rs | 19 +- src/column.rs | 117 ++++++++---- src/fonts.rs | 15 +- src/nav.rs | 241 ++++++++++++++++++------- src/route.rs | 51 ++++++ src/timeline/kind.rs | 30 +++ src/timeline/mod.rs | 6 +- src/timeline/route.rs | 6 +- src/ui/anim.rs | 21 +++ src/ui/profile/preview.rs | 26 +++ src/ui/side_panel.rs | 4 +- 12 files changed, 422 insertions(+), 114 deletions(-) create mode 100644 assets/icons/column_delete_icon_4x.png diff --git a/assets/icons/column_delete_icon_4x.png b/assets/icons/column_delete_icon_4x.png new file mode 100644 index 0000000000000000000000000000000000000000..ae8860bf6977bb308e8378d73bc9488e971957df GIT binary patch literal 806 zcmV+>1KIqEP)@~0drDELIAGL9O(c600d`2O+f$vv5yPK~#7F?VGW3 z)G!c+e>2oKp}&Iq8Yn5DrJ_hdohN`70FxIWX(=*8M?pcAf`%(7uB1vEF>^haynxu9 zC^L+sJ+!i9*|ENvots(9mi%iat!y7qsZ=VJ^jK@(ll`&%;cI-vl;8&2^t8x8gA8!5 z(a!w ztmCEe<`Fdy5d1mT@zUt>2%BylFU{UOB6u%A7kNWJzS;DQ>=*-Nr(_%S{w2@rf= z+96;BztmIDgky(+K0bE^U7ZFBa?b&p338#lM(^>@5Qh=6NkBt`%u_zD7z5Ohr(~CQIDU{lM*@=mWyUq?71=+Nwf1=zu<0bAl k**6;RYg8(gN~J%40Ttn9aZu6DwEzGB07*qoM6N<$f?J$o*8l(j literal 0 HcmV?d00001 diff --git a/src/app.rs b/src/app.rs index 0753a96..ce3c234 100644 --- a/src/app.rs +++ b/src/app.rs @@ -247,7 +247,7 @@ fn try_process_event(damus: &mut Damus, ctx: &egui::Context) -> Result<()> { if let Err(err) = Timeline::poll_notes_into_view( timeline_ind, - &mut damus.columns.timelines, + damus.columns.timelines_mut(), &damus.ndb, &txn, &mut damus.unknown_ids, @@ -490,6 +490,8 @@ fn update_damus(damus: &mut Damus, ctx: &egui::Context) { if let Err(err) = try_process_event(damus, ctx) { error!("error processing event: {}", err); } + + damus.columns.attempt_perform_deletion_request(); } fn process_event(damus: &mut Damus, _subid: &str, event: &str) { @@ -952,7 +954,7 @@ fn render_damus_desktop(ctx: &egui::Context, app: &mut Damus) { puffin::profile_function!(); let screen_size = ctx.screen_rect().width(); - let calc_panel_width = (screen_size / app.columns.columns().len() as f32) - 30.0; + let calc_panel_width = (screen_size / app.columns.num_columns() as f32) - 30.0; let min_width = 320.0; let need_scroll = calc_panel_width < min_width; let panel_sizes = if need_scroll { @@ -965,18 +967,18 @@ fn render_damus_desktop(ctx: &egui::Context, app: &mut Damus) { ui.spacing_mut().item_spacing.x = 0.0; if need_scroll { egui::ScrollArea::horizontal().show(ui, |ui| { - timelines_view(ui, panel_sizes, app, app.columns.columns().len()); + timelines_view(ui, panel_sizes, app); }); } else { - timelines_view(ui, panel_sizes, app, app.columns.columns().len()); + timelines_view(ui, panel_sizes, app); } }); } -fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, columns: usize) { +fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus) { StripBuilder::new(ui) .size(Size::exact(ui::side_panel::SIDE_PANEL_WIDTH)) - .sizes(sizes, columns) + .sizes(sizes, app.columns.num_columns()) .clip(true) .horizontal(|mut strip| { strip.cell(|ui| { @@ -1000,11 +1002,10 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, columns: usiz ); }); - let n_cols = app.columns.columns().len(); - for column_ind in 0..n_cols { + for col_index in 0..app.columns.num_columns() { strip.cell(|ui| { let rect = ui.available_rect_before_wrap(); - nav::render_nav(column_ind, app, ui); + nav::render_nav(col_index, app, ui); // vertical line ui.painter().vline( diff --git a/src/column.rs b/src/column.rs index b5f971c..f826ab5 100644 --- a/src/column.rs +++ b/src/column.rs @@ -1,6 +1,8 @@ use crate::route::{Route, Router}; use crate::timeline::{Timeline, TimelineId}; +use indexmap::IndexMap; use std::iter::Iterator; +use std::sync::atomic::{AtomicU32, Ordering}; use tracing::warn; pub struct Column { @@ -25,16 +27,18 @@ impl Column { #[derive(Default)] pub struct Columns { /// Columns are simply routers into settings, timelines, etc - columns: Vec, + columns: IndexMap, /// Timeline state is not tied to routing logic separately, so that /// different columns can navigate to and from settings to timelines, /// etc. - pub timelines: Vec, + pub timelines: IndexMap, /// The selected column for key navigation selected: i32, + should_delete_column_at_index: Option, } +static UIDS: AtomicU32 = AtomicU32::new(0); impl Columns { pub fn new() -> Self { @@ -42,25 +46,38 @@ impl Columns { } pub fn add_timeline(&mut self, timeline: Timeline) { + let id = Self::get_new_id(); let routes = vec![Route::timeline(timeline.id)]; - self.timelines.push(timeline); - self.columns.push(Column::new(routes)) + self.timelines.insert(id, timeline); + self.columns.insert(id, Column::new(routes)); } - pub fn add_timeline_to_column(&mut self, col: usize, timeline: Timeline) -> bool { - if let Some(column) = self.columns.get_mut(col) { - column - .router_mut() - .route_to_replaced(Route::timeline(timeline.id)); - self.timelines.push(timeline); - true - } else { - false - } + pub fn add_timeline_to_column(&mut self, col: usize, timeline: Timeline) { + let col_id = self.get_column_id_at_index(col); + self.column_mut(col) + .router_mut() + .route_to_replaced(Route::timeline(timeline.id)); + self.timelines.insert(col_id, timeline); } pub fn new_column_picker(&mut self) { - self.columns.push(Column::new(vec![Route::AddColumn])); + self.add_column(Column::new(vec![Route::AddColumn])); + } + + fn get_new_id() -> u32 { + UIDS.fetch_add(1, Ordering::Relaxed) + } + + pub fn add_column(&mut self, column: Column) { + self.columns.insert(Self::get_new_id(), column); + } + + pub fn columns_mut(&mut self) -> Vec<&mut Column> { + self.columns.values_mut().collect() + } + + pub fn num_columns(&self) -> usize { + self.columns.len() } // Get the first router in the columns if there are columns present. @@ -70,49 +87,66 @@ impl Columns { self.new_column_picker(); } self.columns - .get_mut(0) + .get_index_mut(0) .expect("There should be at least one column") + .1 .router_mut() } - pub fn columns_mut(&mut self) -> &mut Vec { - &mut self.columns - } - pub fn timeline_mut(&mut self, timeline_ind: usize) -> &mut Timeline { - &mut self.timelines[timeline_ind] + self.timelines + .get_index_mut(timeline_ind) + .expect("expected index to be in bounds") + .1 } pub fn column(&self, ind: usize) -> &Column { - &self.columns()[ind] + self.columns + .get_index(ind) + .expect("Expected index to be in bounds") + .1 } - pub fn columns(&self) -> &Vec { - &self.columns + pub fn columns(&self) -> Vec<&Column> { + self.columns.values().collect() + } + + pub fn get_column_id_at_index(&self, ind: usize) -> u32 { + *self + .columns + .get_index(ind) + .expect("expected index to be within bounds") + .0 } pub fn selected(&mut self) -> &mut Column { - &mut self.columns[self.selected as usize] + self.columns + .get_index_mut(self.selected as usize) + .expect("Expected selected index to be in bounds") + .1 } - pub fn timelines_mut(&mut self) -> &mut Vec { - &mut self.timelines + pub fn timelines_mut(&mut self) -> Vec<&mut Timeline> { + self.timelines.values_mut().collect() } - pub fn timelines(&self) -> &Vec { - &self.timelines + pub fn timelines(&self) -> Vec<&Timeline> { + self.timelines.values().collect() } pub fn find_timeline_mut(&mut self, id: TimelineId) -> Option<&mut Timeline> { - self.timelines_mut().iter_mut().find(|tl| tl.id == id) + self.timelines_mut().into_iter().find(|tl| tl.id == id) } pub fn find_timeline(&self, id: TimelineId) -> Option<&Timeline> { - self.timelines().iter().find(|tl| tl.id == id) + self.timelines().into_iter().find(|tl| tl.id == id) } pub fn column_mut(&mut self, ind: usize) -> &mut Column { - &mut self.columns[ind] + self.columns + .get_index_mut(ind) + .expect("Expected index to be in bounds") + .1 } pub fn select_down(&mut self) { @@ -136,4 +170,23 @@ impl Columns { } self.selected += 1; } + + pub fn request_deletion_at_index(&mut self, index: usize) { + self.should_delete_column_at_index = Some(index); + } + + pub fn attempt_perform_deletion_request(&mut self) { + if let Some(index) = self.should_delete_column_at_index { + if let Some((key, _)) = self.columns.get_index_mut(index) { + self.timelines.shift_remove(key); + } + + self.columns.shift_remove_index(index); + self.should_delete_column_at_index = None; + + if self.columns.is_empty() { + self.new_column_picker(); + } + } + } } diff --git a/src/fonts.rs b/src/fonts.rs index 6ec5174..220e01d 100644 --- a/src/fonts.rs +++ b/src/fonts.rs @@ -4,12 +4,13 @@ use tracing::debug; pub enum NamedFontFamily { Medium, + Bold, } impl NamedFontFamily { pub fn as_str(&mut self) -> &'static str { match self { - //Self::Bold => "bold", + Self::Bold => "bold", Self::Medium => "medium", } } @@ -43,7 +44,7 @@ pub fn setup_fonts(ctx: &egui::Context) { "DejaVuSans".to_owned(), FontData::from_static(include_bytes!("../assets/fonts/DejaVuSansSansEmoji.ttf")), ); - /* + font_data.insert( "OnestBold".to_owned(), FontData::from_static(include_bytes!( @@ -51,6 +52,7 @@ pub fn setup_fonts(ctx: &egui::Context) { )), ); + /* font_data.insert( "DejaVuSansBold".to_owned(), FontData::from_static(include_bytes!( @@ -119,7 +121,10 @@ pub fn setup_fonts(ctx: &egui::Context) { medium.extend(base_fonts.clone()); let mut mono = vec!["Inconsolata".to_owned()]; - mono.extend(base_fonts); + mono.extend(base_fonts.clone()); + + let mut bold = vec!["OnestBold".to_owned()]; + bold.extend(base_fonts); families.insert(egui::FontFamily::Proportional, proportional); families.insert(egui::FontFamily::Monospace, mono); @@ -127,6 +132,10 @@ pub fn setup_fonts(ctx: &egui::Context) { egui::FontFamily::Name(NamedFontFamily::Medium.as_str().into()), medium, ); + families.insert( + egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()), + bold, + ); debug!("fonts: {:?}", families); diff --git a/src/nav.rs b/src/nav.rs index 0702efb..7e1422e 100644 --- a/src/nav.rs +++ b/src/nav.rs @@ -1,5 +1,7 @@ use crate::{ account_manager::render_accounts_route, + app_style::{get_font_size, NotedeckTextStyle}, + fonts::NamedFontFamily, relay_pool_manager::RelayPoolManager, route::Route, thread::thread_unsubscribe, @@ -7,91 +9,105 @@ use crate::{ ui::{ self, add_column::{AddColumnResponse, AddColumnView}, + anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, note::PostAction, RelayView, View, }, Damus, }; +use egui::{pos2, Color32, InnerResponse}; use egui_nav::{Nav, NavAction}; pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { + let col_id = app.columns.get_column_id_at_index(col); // 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()) + let routes = app + .columns() + .column(col) + .router() + .routes() + .iter() + .map(|r| r.get_titled_route(&app.columns, &app.ndb)) + .collect(); + let nav_response = Nav::new(routes) .navigating(app.columns_mut().column_mut(col).router_mut().navigating) .returning(app.columns_mut().column_mut(col).router_mut().returning) - .title(false) - .show_mut(ui, |ui, nav| match nav.top() { - Route::Timeline(tlr) => render_timeline_route( - &app.ndb, - &mut app.columns, - &mut app.pool, - &mut app.drafts, - &mut app.img_cache, - &mut app.note_cache, - &mut app.threads, - &mut app.accounts, - *tlr, - col, - app.textmode, - ui, - ), - Route::Accounts(amr) => { - render_accounts_route( - ui, + .title(48.0, title_bar) + .show_mut(col_id, ui, |ui, nav| { + let column = app.columns.column_mut(col); + match &nav.top().route { + Route::Timeline(tlr) => render_timeline_route( &app.ndb, - col, &mut app.columns, - &mut app.img_cache, - &mut app.accounts, - &mut app.view_state.login, - *amr, - ); - None - } - Route::Relays => { - let manager = RelayPoolManager::new(app.pool_mut()); - 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.pool, + &mut app.drafts, &mut app.img_cache, &mut app.note_cache, - kp, - ) - .ui(&txn, ui); - - if let Some(action) = post_response.action { - PostAction::execute(kp, &action, &mut app.pool, draft, |np, seckey| { - np.to_note(seckey) - }); - app.columns_mut().column_mut(col).router_mut().go_back(); + &mut app.threads, + &mut app.accounts, + *tlr, + col, + app.textmode, + ui, + ), + Route::Accounts(amr) => { + render_accounts_route( + ui, + &app.ndb, + col, + &mut app.columns, + &mut app.img_cache, + &mut app.accounts, + &mut app.view_state.login, + *amr, + ); + None } + Route::Relays => { + let manager = RelayPoolManager::new(app.pool_mut()); + RelayView::new(manager).ui(ui); + None + } + Route::ComposeNote => { + let kp = app.accounts.selected_or_first_nsec()?; + let draft = app.drafts.compose_mut(); - None - } - Route::AddColumn => { - let resp = AddColumnView::new(&app.ndb, app.accounts.get_selected_account()).ui(ui); + 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(resp) = resp { - match resp { - AddColumnResponse::Timeline(timeline) => { - let id = timeline.id; - if app.columns_mut().add_timeline_to_column(col, timeline) { + if let Some(action) = post_response.action { + PostAction::execute(kp, &action, &mut app.pool, draft, |np, seckey| { + np.to_note(seckey) + }); + column.router_mut().go_back(); + } + + None + } + Route::AddColumn => { + let resp = + AddColumnView::new(&app.ndb, app.accounts.get_selected_account()).ui(ui); + + if let Some(resp) = resp { + match resp { + AddColumnResponse::Timeline(timeline) => { + let id = timeline.id; + app.columns_mut().add_timeline_to_column(col, timeline); app.subscribe_new_timeline(id); } - } - }; + }; + } + None } - None } }); @@ -128,4 +144,103 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { cur_router.remove_previous_route(); } } + + if let Some(title_response) = nav_response.title_response { + match title_response { + TitleResponse::RemoveColumn => { + app.columns_mut().request_deletion_at_index(col); + } + } + } +} + +fn title_bar( + ui: &mut egui::Ui, + title_name: String, + allocated_response: egui::Response, +) -> egui::InnerResponse> { + let icon_width = 32.0; + let padding = 16.0; + title(ui, title_name, allocated_response.rect, icon_width, padding); + let button_resp = delete_column_button(ui, allocated_response, icon_width, padding); + let title_response = if button_resp.clicked() { + Some(TitleResponse::RemoveColumn) + } else { + None + }; + + InnerResponse::new(title_response, button_resp) +} + +fn delete_column_button( + ui: &mut egui::Ui, + title_bar_resp: egui::Response, + icon_width: f32, + padding: f32, +) -> egui::Response { + let img_size = 16.0; + let max_size = icon_width * ICON_EXPANSION_MULTIPLE; + + let img_data = egui::include_image!("../assets/icons/column_delete_icon_4x.png"); + let img = egui::Image::new(img_data).max_width(img_size); + + let button_rect = { + let titlebar_rect = title_bar_resp.rect; + let titlebar_width = titlebar_rect.width(); + let titlebar_center = titlebar_rect.center(); + let button_center_y = titlebar_center.y; + let button_center_x = + titlebar_center.x + (titlebar_width / 2.0) - (max_size / 2.0) - padding; + egui::Rect::from_center_size( + pos2(button_center_x, button_center_y), + egui::vec2(max_size, max_size), + ) + }; + + let helper = AnimationHelper::new_from_rect(ui, "delete-column-button", button_rect); + + let cur_img_size = helper.scale_1d_pos(img_size); + + let animation_rect = helper.get_animation_rect(); + let animation_resp = helper.take_animation_response(); + if title_bar_resp.union(animation_resp.clone()).hovered() { + img.paint_at(ui, animation_rect.shrink((max_size - cur_img_size) / 2.0)); + } + + animation_resp +} + +fn title( + ui: &mut egui::Ui, + title_name: String, + titlebar_rect: egui::Rect, + icon_width: f32, + padding: f32, +) { + let painter = ui.painter_at(titlebar_rect); + + let font = egui::FontId::new( + get_font_size(ui.ctx(), &NotedeckTextStyle::Body), + egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()), + ); + + let max_title_width = titlebar_rect.width() - icon_width - padding * 2.; + let title_galley = + ui.fonts(|f| f.layout(title_name, font, ui.visuals().text_color(), max_title_width)); + + let pos = { + let titlebar_center = titlebar_rect.center(); + let titlebar_width = titlebar_rect.width(); + let text_height = title_galley.rect.height(); + + let galley_pos_x = titlebar_center.x - (titlebar_width / 2.) + padding; + let galley_pos_y = titlebar_center.y - (text_height / 2.); + pos2(galley_pos_x, galley_pos_y) + }; + + painter.galley(pos, title_galley, Color32::WHITE); +} + +enum TitleResponse { + RemoveColumn, } diff --git a/src/route.rs b/src/route.rs index 22828ab..9b622c9 100644 --- a/src/route.rs +++ b/src/route.rs @@ -1,9 +1,12 @@ use enostr::NoteId; +use nostrdb::Ndb; use std::fmt::{self}; use crate::{ account_manager::AccountsRoute, + column::Columns, timeline::{TimelineId, TimelineRoute}, + ui::profile::preview::get_note_users_displayname_string, }; /// App routing. These describe different places you can go inside Notedeck. @@ -16,6 +19,18 @@ pub enum Route { AddColumn, } +#[derive(Clone)] +pub struct TitledRoute { + pub route: Route, + pub title: String, +} + +impl fmt::Display for TitledRoute { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.title) + } +} + impl Route { pub fn timeline(timeline_id: TimelineId) -> Self { Route::Timeline(TimelineRoute::Timeline(timeline_id)) @@ -52,6 +67,42 @@ impl Route { pub fn add_account() -> Self { Route::Accounts(AccountsRoute::AddAccount) } + + pub fn get_titled_route(&self, columns: &Columns, ndb: &Ndb) -> TitledRoute { + let title = match self { + Route::Timeline(tlr) => match tlr { + TimelineRoute::Timeline(id) => { + let timeline = columns + .find_timeline(*id) + .expect("expected to find timeline"); + timeline.kind.to_title(ndb) + } + TimelineRoute::Thread(id) => { + format!("{}'s Thread", get_note_users_displayname_string(ndb, id)) + } + TimelineRoute::Reply(id) => { + format!("{}'s Reply", get_note_users_displayname_string(ndb, id)) + } + TimelineRoute::Quote(id) => { + format!("{}'s Quote", get_note_users_displayname_string(ndb, id)) + } + }, + + Route::Relays => "Relays".to_owned(), + + Route::Accounts(amr) => match amr { + AccountsRoute::Accounts => "Accounts".to_owned(), + AccountsRoute::AddAccount => "Add Account".to_owned(), + }, + Route::ComposeNote => "Compose Note".to_owned(), + Route::AddColumn => "Add Column".to_owned(), + }; + + TitledRoute { + title, + route: *self, + } + } } // TODO: add this to egui-nav so we don't have to deal with returning diff --git a/src/timeline/kind.rs b/src/timeline/kind.rs index 016706e..e39afa1 100644 --- a/src/timeline/kind.rs +++ b/src/timeline/kind.rs @@ -2,6 +2,7 @@ use crate::error::{Error, FilterError}; use crate::filter; use crate::filter::FilterState; use crate::timeline::Timeline; +use crate::ui::profile::preview::get_profile_displayname_string; use enostr::{Filter, Pubkey}; use nostrdb::{Ndb, Transaction}; use std::fmt::Display; @@ -150,4 +151,33 @@ impl TimelineKind { } } } + + pub fn to_title(&self, ndb: &Ndb) -> String { + match self { + TimelineKind::List(list_kind) => match list_kind { + ListKind::Contact(pubkey_source) => match pubkey_source { + PubkeySource::Explicit(pubkey) => format!( + "{}'s Home Timeline", + get_profile_displayname_string(ndb, pubkey) + ), + PubkeySource::DeckAuthor => "Your Home Timeline".to_owned(), + }, + }, + TimelineKind::Notifications(pubkey_source) => match pubkey_source { + PubkeySource::DeckAuthor => "Your Notifications".to_owned(), + PubkeySource::Explicit(pk) => format!( + "{}'s Notifications", + get_profile_displayname_string(ndb, pk) + ), + }, + TimelineKind::Profile(pubkey_source) => match pubkey_source { + PubkeySource::DeckAuthor => "Your Profile".to_owned(), + PubkeySource::Explicit(pk) => { + format!("{}'s Profile", get_profile_displayname_string(ndb, pk)) + } + }, + TimelineKind::Universe => "Universe".to_owned(), + TimelineKind::Generic => "Custom Filter".to_owned(), + } + } } diff --git a/src/timeline/mod.rs b/src/timeline/mod.rs index f5365c8..e271fee 100644 --- a/src/timeline/mod.rs +++ b/src/timeline/mod.rs @@ -241,13 +241,15 @@ impl Timeline { pub fn poll_notes_into_view( timeline_idx: usize, - timelines: &mut [Timeline], + mut timelines: Vec<&mut Timeline>, ndb: &Ndb, txn: &Transaction, unknown_ids: &mut UnknownIds, note_cache: &mut NoteCache, ) -> Result<()> { - let timeline = &mut timelines[timeline_idx]; + let timeline = timelines + .get_mut(timeline_idx) + .ok_or(Error::TimelineNotFound)?; let sub = timeline.subscription.ok_or(Error::no_active_sub())?; let new_note_ids = ndb.poll_for_notes(sub, 500); diff --git a/src/timeline/route.rs b/src/timeline/route.rs index 71b9774..7c1367c 100644 --- a/src/timeline/route.rs +++ b/src/timeline/route.rs @@ -58,7 +58,8 @@ pub fn render_timeline_route( .ui(ui) { let txn = Transaction::new(ndb).expect("txn"); - let router = columns.columns_mut()[col].router_mut(); + let mut cur_column = columns.columns_mut(); + let router = cur_column[col].router_mut(); bar_action.execute_and_process_result(ndb, router, threads, note_cache, pool, &txn); } @@ -73,7 +74,8 @@ pub fn render_timeline_route( .ui(ui) { let txn = Transaction::new(ndb).expect("txn"); - let router = columns.columns_mut()[col].router_mut(); + let mut cur_column = columns.columns_mut(); + let router = cur_column[col].router_mut(); bar_action.execute_and_process_result(ndb, router, threads, note_cache, pool, &txn); } diff --git a/src/ui/anim.rs b/src/ui/anim.rs index 339e0dc..ff5bd52 100644 --- a/src/ui/anim.rs +++ b/src/ui/anim.rs @@ -60,6 +60,27 @@ impl AnimationHelper { } } + pub fn new_from_rect( + ui: &mut egui::Ui, + animation_name: impl std::hash::Hash, + animation_rect: egui::Rect, + ) -> Self { + let id = ui.id().with(animation_name); + let response = ui.allocate_rect(animation_rect, Sense::click()); + + let animation_progress = + ui.ctx() + .animate_bool_with_time(id, response.hovered(), ANIM_SPEED); + + Self { + rect: animation_rect, + center: animation_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; diff --git a/src/ui/profile/preview.rs b/src/ui/profile/preview.rs index 25ef49e..223ae17 100644 --- a/src/ui/profile/preview.rs +++ b/src/ui/profile/preview.rs @@ -6,6 +6,7 @@ use crate::{colors, images, DisplayName}; use egui::load::TexturePoll; use egui::{Frame, RichText, Sense, Widget}; use egui_extras::Size; +use enostr::NoteId; use nostrdb::ProfileRecord; pub struct ProfilePreview<'a, 'cache> { @@ -256,3 +257,28 @@ fn about_section_widget<'a>(profile: &'a ProfileRecord<'a>) -> impl egui::Widget } } } + +fn get_display_name_as_string(profile: Option<&'_ ProfileRecord<'_>>) -> String { + let display_name = get_display_name(profile); + match display_name { + DisplayName::One(n) => n.to_string(), + DisplayName::Both { display_name, .. } => display_name.to_string(), + } +} + +pub fn get_profile_displayname_string(ndb: &nostrdb::Ndb, pk: &enostr::Pubkey) -> String { + let txn = nostrdb::Transaction::new(ndb).expect("Transaction should have worked"); + let profile = ndb.get_profile_by_pubkey(&txn, pk.bytes()).ok(); + get_display_name_as_string(profile.as_ref()) +} + +pub fn get_note_users_displayname_string(ndb: &nostrdb::Ndb, id: &NoteId) -> String { + let txn = nostrdb::Transaction::new(ndb).expect("Transaction should have worked"); + let note = ndb.get_note_by_id(&txn, id.bytes()); + let profile = if let Ok(note) = note { + ndb.get_profile_by_pubkey(&txn, note.pubkey()).ok() + } else { + None + }; + get_display_name_as_string(profile.as_ref()) +} diff --git a/src/ui/side_panel.rs b/src/ui/side_panel.rs index e676222..bb779ca 100644 --- a/src/ui/side_panel.rs +++ b/src/ui/side_panel.rs @@ -370,9 +370,7 @@ mod preview { impl DesktopSidePanelPreview { fn new() -> Self { let mut app = test_data::test_app(); - app.columns - .columns_mut() - .push(Column::new(vec![Route::accounts()])); + app.columns.add_column(Column::new(vec![Route::accounts()])); DesktopSidePanelPreview { app } } } From 0d994172a0456c09a5265c3c9434c114f78f165b Mon Sep 17 00:00:00 2001 From: kernelkind Date: Mon, 7 Oct 2024 13:44:35 -0400 Subject: [PATCH 09/14] unsubscribe timeline on deletion Signed-off-by: kernelkind --- src/column.rs | 5 +++++ src/nav.rs | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/column.rs b/src/column.rs index f826ab5..62d45cd 100644 --- a/src/column.rs +++ b/src/column.rs @@ -149,6 +149,11 @@ impl Columns { .1 } + pub fn find_timeline_for_column_index(&self, ind: usize) -> Option<&Timeline> { + let col_id = self.get_column_id_at_index(ind); + self.timelines.get(&col_id) + } + pub fn select_down(&mut self) { warn!("todo: implement select_down"); } diff --git a/src/nav.rs b/src/nav.rs index 7e1422e..6af9945 100644 --- a/src/nav.rs +++ b/src/nav.rs @@ -18,6 +18,7 @@ use crate::{ use egui::{pos2, Color32, InnerResponse}; use egui_nav::{Nav, NavAction}; +use tracing::{error, info}; pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { let col_id = app.columns.get_column_id_at_index(col); @@ -149,6 +150,20 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { match title_response { TitleResponse::RemoveColumn => { app.columns_mut().request_deletion_at_index(col); + let tl = app.columns().find_timeline_for_column_index(col); + if let Some(timeline) = tl { + if let Some(sub_id) = timeline.subscription { + if let Err(e) = app.ndb.unsubscribe(sub_id) { + error!("unsubscribe error: {}", e); + } else { + info!( + "successfully unsubscribed from timeline {} with sub id {}", + timeline.id, + sub_id.id() + ); + } + } + } } } } From ee5aa3246964d869fa7003eb43cb2341d7b115cd Mon Sep 17 00:00:00 2001 From: kernelkind Date: Tue, 8 Oct 2024 19:10:04 -0400 Subject: [PATCH 10/14] fix deck author bug & rename titles Signed-off-by: kernelkind --- src/timeline/kind.rs | 15 +++++++-------- src/timeline/mod.rs | 4 +--- src/ui/add_column.rs | 26 ++++++++++++++++++-------- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/timeline/kind.rs b/src/timeline/kind.rs index e39afa1..c48a927 100644 --- a/src/timeline/kind.rs +++ b/src/timeline/kind.rs @@ -137,7 +137,7 @@ impl TimelineKind { )); } - match Timeline::contact_list(&results[0].note) { + match Timeline::contact_list(&results[0].note, pk_src.clone()) { Err(Error::Filter(FilterError::EmptyContactList)) => Some(Timeline::new( TimelineKind::contact_list(pk_src), FilterState::needs_remote(vec![contact_filter]), @@ -156,22 +156,21 @@ impl TimelineKind { match self { TimelineKind::List(list_kind) => match list_kind { ListKind::Contact(pubkey_source) => match pubkey_source { - PubkeySource::Explicit(pubkey) => format!( - "{}'s Home Timeline", - get_profile_displayname_string(ndb, pubkey) - ), - PubkeySource::DeckAuthor => "Your Home Timeline".to_owned(), + PubkeySource::Explicit(pubkey) => { + format!("{}'s Contacts", get_profile_displayname_string(ndb, pubkey)) + } + PubkeySource::DeckAuthor => "Contacts".to_owned(), }, }, TimelineKind::Notifications(pubkey_source) => match pubkey_source { - PubkeySource::DeckAuthor => "Your Notifications".to_owned(), + PubkeySource::DeckAuthor => "Notifications".to_owned(), PubkeySource::Explicit(pk) => format!( "{}'s Notifications", get_profile_displayname_string(ndb, pk) ), }, TimelineKind::Profile(pubkey_source) => match pubkey_source { - PubkeySource::DeckAuthor => "Your Profile".to_owned(), + PubkeySource::DeckAuthor => "Profile".to_owned(), PubkeySource::Explicit(pk) => { format!("{}'s Profile", get_profile_displayname_string(ndb, pk)) } diff --git a/src/timeline/mod.rs b/src/timeline/mod.rs index e271fee..7259337 100644 --- a/src/timeline/mod.rs +++ b/src/timeline/mod.rs @@ -8,7 +8,6 @@ use std::fmt; use std::sync::atomic::{AtomicU32, Ordering}; use egui_virtual_list::VirtualList; -use enostr::Pubkey; use nostrdb::{Ndb, Note, Subscription, Transaction}; use std::cell::RefCell; use std::hash::Hash; @@ -180,9 +179,8 @@ pub struct Timeline { impl Timeline { /// Create a timeline from a contact list - pub fn contact_list(contact_list: &Note) -> Result { + pub fn contact_list(contact_list: &Note, pk_src: PubkeySource) -> Result { let filter = filter::filter_from_tags(contact_list)?.into_follow_filter(); - let pk_src = PubkeySource::Explicit(Pubkey::new(*contact_list.pubkey())); Ok(Timeline::new( TimelineKind::contact_list(pk_src), diff --git a/src/ui/add_column.rs b/src/ui/add_column.rs index c75e3c4..298addc 100644 --- a/src/ui/add_column.rs +++ b/src/ui/add_column.rs @@ -14,7 +14,7 @@ pub enum AddColumnResponse { Timeline(Timeline), } -#[derive(Clone)] +#[derive(Clone, Debug)] enum AddColumnOption { Universe, Notification(PubkeySource), @@ -22,17 +22,23 @@ enum AddColumnOption { } impl AddColumnOption { - pub fn take_as_response(self, ndb: &Ndb) -> Option { + pub fn take_as_response( + self, + ndb: &Ndb, + cur_account: Option<&UserAccount>, + ) -> Option { match self { AddColumnOption::Universe => TimelineKind::Universe .into_timeline(ndb, None) .map(AddColumnResponse::Timeline), AddColumnOption::Notification(pubkey) => TimelineKind::Notifications(pubkey) - .into_timeline(ndb, None) - .map(AddColumnResponse::Timeline), - AddColumnOption::Home(pubkey) => TimelineKind::contact_list(pubkey) - .into_timeline(ndb, None) + .into_timeline(ndb, cur_account.map(|a| a.pubkey.bytes())) .map(AddColumnResponse::Timeline), + AddColumnOption::Home(pubkey) => { + let tlk = TimelineKind::contact_list(pubkey); + tlk.into_timeline(ndb, cur_account.map(|a| a.pubkey.bytes())) + .map(AddColumnResponse::Timeline) + } } } } @@ -52,7 +58,7 @@ impl<'a> AddColumnView<'a> { for column_option_data in self.get_column_options() { let option = column_option_data.option.clone(); if self.column_option_ui(ui, column_option_data).clicked() { - selected_option = option.take_as_response(self.ndb); + selected_option = option.take_as_response(self.ndb, self.cur_account); } ui.add(Separator::default().spacing(0.0)); @@ -172,7 +178,11 @@ impl<'a> AddColumnView<'a> { }); if let Some(acc) = self.cur_account { - let source = PubkeySource::Explicit(acc.pubkey); + let source = if acc.secret_key.is_some() { + PubkeySource::DeckAuthor + } else { + PubkeySource::Explicit(acc.pubkey) + }; vec.push(ColumnOptionData { title: "Home timeline", From 983640b85a06bce2d026073ca9ce4820fe45c174 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Tue, 8 Oct 2024 21:37:16 -0400 Subject: [PATCH 11/14] tmp: kernelkind/egui-nav Signed-off-by: kernelkind --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 246264b..bfa7e4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1148,7 +1148,7 @@ dependencies = [ [[package]] name = "egui_nav" version = "0.1.0" -source = "git+https://github.com/kernelkind/egui-nav?rev=6a7b1cf735e8a770b2510fbac06837d47c44e2c0#6a7b1cf735e8a770b2510fbac06837d47c44e2c0" +source = "git+https://github.com/kernelkind/egui-nav?rev=1fbaf0811c67b674068ba69c8aa92e8e8c2ed1da#1fbaf0811c67b674068ba69c8aa92e8e8c2ed1da" dependencies = [ "egui", "egui_extras", diff --git a/Cargo.toml b/Cargo.toml index 2f1836b..a8c2e91 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ eframe = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da egui_extras = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "egui_extras", features = ["all_loaders"] } ehttp = "0.2.0" egui_tabs = { git = "https://github.com/damus-io/egui-tabs", branch = "egui-0.28" } -egui_nav = { git = "https://github.com/kernelkind/egui-nav", rev = "6a7b1cf735e8a770b2510fbac06837d47c44e2c0" } +egui_nav = { git = "https://github.com/kernelkind/egui-nav", rev = "1fbaf0811c67b674068ba69c8aa92e8e8c2ed1da" } egui_virtual_list = { git = "https://github.com/jb55/hello_egui", branch = "egui-0.28", package = "egui_virtual_list" } reqwest = { version = "0.12.4", default-features = false, features = [ "rustls-tls-native-roots" ] } image = { version = "0.25", features = ["jpeg", "png", "webp"] } From a2fc754e1b1b15eaeaab1578b9f80a6dc97b4928 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Tue, 8 Oct 2024 21:37:34 -0400 Subject: [PATCH 12/14] updated back arrow Signed-off-by: kernelkind --- src/nav.rs | 102 ++++++++++++++++++++++++++++++++++++++++++------- src/ui/anim.rs | 4 ++ 2 files changed, 92 insertions(+), 14 deletions(-) diff --git a/src/nav.rs b/src/nav.rs index 6af9945..bca1d03 100644 --- a/src/nav.rs +++ b/src/nav.rs @@ -16,8 +16,8 @@ use crate::{ Damus, }; -use egui::{pos2, Color32, InnerResponse}; -use egui_nav::{Nav, NavAction}; +use egui::{pos2, Color32, InnerResponse, Stroke}; +use egui_nav::{Nav, NavAction, TitleBarResponse}; use tracing::{error, info}; pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { @@ -171,25 +171,100 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { fn title_bar( ui: &mut egui::Ui, - title_name: String, allocated_response: egui::Response, -) -> egui::InnerResponse> { + title_name: String, + back_name: Option, +) -> egui::InnerResponse> { let icon_width = 32.0; - let padding = 16.0; - title(ui, title_name, allocated_response.rect, icon_width, padding); - let button_resp = delete_column_button(ui, allocated_response, icon_width, padding); - let title_response = if button_resp.clicked() { + let padding_external = 16.0; + let padding_internal = 8.0; + let has_back = back_name.is_some(); + + let (spacing_rect, titlebar_rect) = allocated_response + .rect + .split_left_right_at_x(allocated_response.rect.left() + padding_external); + ui.advance_cursor_after_rect(spacing_rect); + + let (titlebar_resp, maybe_button_resp) = if has_back { + let (button_rect, titlebar_rect) = titlebar_rect + .split_left_right_at_x(allocated_response.rect.left() + icon_width + padding_external); + ( + allocated_response.with_new_rect(titlebar_rect), + Some(back_button(ui, button_rect)), + ) + } else { + (allocated_response, None) + }; + + title( + ui, + title_name, + titlebar_resp.rect, + icon_width, + if has_back { + padding_internal + } else { + padding_external + }, + ); + + let delete_button_resp = delete_column_button(ui, titlebar_resp, icon_width, padding_external); + let title_response = if delete_button_resp.clicked() { Some(TitleResponse::RemoveColumn) } else { None }; - InnerResponse::new(title_response, button_resp) + let titlebar_resp = TitleBarResponse { + title_response, + go_back: maybe_button_resp.map_or(false, |r| r.clicked()), + }; + + InnerResponse::new(titlebar_resp, delete_button_resp) +} + +fn back_button(ui: &mut egui::Ui, button_rect: egui::Rect) -> egui::Response { + let horizontal_length = 10.0; + let arrow_length = 5.0; + + let helper = AnimationHelper::new_from_rect(ui, "note-compose-button", button_rect); + let painter = ui.painter_at(helper.get_animation_rect()); + let stroke = Stroke::new(1.5, ui.visuals().text_color()); + + // Horizontal segment + let left_horizontal_point = pos2(-horizontal_length / 2., 0.); + let right_horizontal_point = pos2(horizontal_length / 2., 0.); + let scaled_left_horizontal_point = helper.scale_pos_from_center(left_horizontal_point); + let scaled_right_horizontal_point = helper.scale_pos_from_center(right_horizontal_point); + + painter.line_segment( + [scaled_left_horizontal_point, scaled_right_horizontal_point], + stroke, + ); + + // Top Arrow + let sqrt_2_over_2 = std::f32::consts::SQRT_2 / 2.; + let right_top_arrow_point = helper.scale_pos_from_center(pos2( + left_horizontal_point.x + (sqrt_2_over_2 * arrow_length), + right_horizontal_point.y + sqrt_2_over_2 * arrow_length, + )); + + let scaled_left_arrow_point = scaled_left_horizontal_point; + painter.line_segment([scaled_left_arrow_point, right_top_arrow_point], stroke); + + let right_bottom_arrow_point = helper.scale_pos_from_center(pos2( + left_horizontal_point.x + (sqrt_2_over_2 * arrow_length), + right_horizontal_point.y - sqrt_2_over_2 * arrow_length, + )); + + painter.line_segment([scaled_left_arrow_point, right_bottom_arrow_point], stroke); + + helper.take_animation_response() } fn delete_column_button( ui: &mut egui::Ui, - title_bar_resp: egui::Response, + allocation_response: egui::Response, icon_width: f32, padding: f32, ) -> egui::Response { @@ -200,7 +275,7 @@ fn delete_column_button( let img = egui::Image::new(img_data).max_width(img_size); let button_rect = { - let titlebar_rect = title_bar_resp.rect; + let titlebar_rect = allocation_response.rect; let titlebar_width = titlebar_rect.width(); let titlebar_center = titlebar_rect.center(); let button_center_y = titlebar_center.y; @@ -218,7 +293,7 @@ fn delete_column_button( let animation_rect = helper.get_animation_rect(); let animation_resp = helper.take_animation_response(); - if title_bar_resp.union(animation_resp.clone()).hovered() { + if allocation_response.union(animation_resp.clone()).hovered() { img.paint_at(ui, animation_rect.shrink((max_size - cur_img_size) / 2.0)); } @@ -245,10 +320,9 @@ fn title( let pos = { let titlebar_center = titlebar_rect.center(); - let titlebar_width = titlebar_rect.width(); let text_height = title_galley.rect.height(); - let galley_pos_x = titlebar_center.x - (titlebar_width / 2.) + padding; + let galley_pos_x = titlebar_rect.left() + padding; let galley_pos_y = titlebar_center.y - (text_height / 2.); pos2(galley_pos_x, galley_pos_y) }; diff --git a/src/ui/anim.rs b/src/ui/anim.rs index ff5bd52..312ab83 100644 --- a/src/ui/anim.rs +++ b/src/ui/anim.rs @@ -114,4 +114,8 @@ impl AnimationHelper { self.center.y + self.scale_1d_pos(y_min), ) } + + pub fn scale_pos_from_center(&self, min_pos: Pos2) -> Pos2 { + self.scale_from_center(min_pos.x, min_pos.y) + } } From b1549a52c730d41fbfd5b6e34aca9c1ec06797e2 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Tue, 8 Oct 2024 21:43:31 -0400 Subject: [PATCH 13/14] tmp: kernelkind/egui-nav Signed-off-by: kernelkind --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bfa7e4a..7d0f110 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1148,7 +1148,7 @@ dependencies = [ [[package]] name = "egui_nav" version = "0.1.0" -source = "git+https://github.com/kernelkind/egui-nav?rev=1fbaf0811c67b674068ba69c8aa92e8e8c2ed1da#1fbaf0811c67b674068ba69c8aa92e8e8c2ed1da" +source = "git+https://github.com/kernelkind/egui-nav?rev=6ba42de2bae384d10e35c532f3856b81d2e9f645#6ba42de2bae384d10e35c532f3856b81d2e9f645" dependencies = [ "egui", "egui_extras", diff --git a/Cargo.toml b/Cargo.toml index a8c2e91..98771d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ eframe = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da egui_extras = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "egui_extras", features = ["all_loaders"] } ehttp = "0.2.0" egui_tabs = { git = "https://github.com/damus-io/egui-tabs", branch = "egui-0.28" } -egui_nav = { git = "https://github.com/kernelkind/egui-nav", rev = "1fbaf0811c67b674068ba69c8aa92e8e8c2ed1da" } +egui_nav = { git = "https://github.com/kernelkind/egui-nav", rev = "6ba42de2bae384d10e35c532f3856b81d2e9f645" } egui_virtual_list = { git = "https://github.com/jb55/hello_egui", branch = "egui-0.28", package = "egui_virtual_list" } reqwest = { version = "0.12.4", default-features = false, features = [ "rustls-tls-native-roots" ] } image = { version = "0.25", features = ["jpeg", "png", "webp"] } From f7c6e2774e71e0dbb21b5fa2b2976756e804b50c Mon Sep 17 00:00:00 2001 From: William Casarin Date: Fri, 11 Oct 2024 12:54:49 +0200 Subject: [PATCH 14/14] update to use upstream egui-nav branch Signed-off-by: William Casarin --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7d0f110..8ca5953 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1148,7 +1148,7 @@ dependencies = [ [[package]] name = "egui_nav" version = "0.1.0" -source = "git+https://github.com/kernelkind/egui-nav?rev=6ba42de2bae384d10e35c532f3856b81d2e9f645#6ba42de2bae384d10e35c532f3856b81d2e9f645" +source = "git+https://github.com/damus-io/egui-nav?rev=6ba42de2bae384d10e35c532f3856b81d2e9f645#6ba42de2bae384d10e35c532f3856b81d2e9f645" dependencies = [ "egui", "egui_extras", diff --git a/Cargo.toml b/Cargo.toml index 98771d5..17e99ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ eframe = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da egui_extras = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "egui_extras", features = ["all_loaders"] } ehttp = "0.2.0" egui_tabs = { git = "https://github.com/damus-io/egui-tabs", branch = "egui-0.28" } -egui_nav = { git = "https://github.com/kernelkind/egui-nav", rev = "6ba42de2bae384d10e35c532f3856b81d2e9f645" } +egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "6ba42de2bae384d10e35c532f3856b81d2e9f645" } egui_virtual_list = { git = "https://github.com/jb55/hello_egui", branch = "egui-0.28", package = "egui_virtual_list" } reqwest = { version = "0.12.4", default-features = false, features = [ "rustls-tls-native-roots" ] } image = { version = "0.25", features = ["jpeg", "png", "webp"] }