prop drag id through responses instead of manual wiring

Signed-off-by: kernelkind <kernelkind@gmail.com>
This commit is contained in:
kernelkind
2025-09-25 13:33:52 -04:00
parent a6d91c43e4
commit d81243f055
16 changed files with 394 additions and 201 deletions

View File

@@ -7,6 +7,7 @@ use notedeck_ui::nip51_set::Nip51SetUiCache;
pub use crate::accounts::route::AccountsResponse;
use crate::app::get_active_columns_mut;
use crate::decks::DecksCache;
use crate::nav::BodyResponse;
use crate::onboarding::Onboarding;
use crate::profile::send_new_contact_list;
use crate::subscriptions::Subscriptions;
@@ -81,7 +82,7 @@ pub fn render_accounts_route(
onboarding: &mut Onboarding,
follow_packs_ui: &mut Nip51SetUiCache,
route: AccountsRoute,
) -> Option<AccountsResponse> {
) -> BodyResponse<AccountsResponse> {
match route {
AccountsRoute::Accounts => AccountsView::new(
app_ctx.ndb,
@@ -90,15 +91,15 @@ pub fn render_accounts_route(
app_ctx.i18n,
)
.ui(ui)
.inner
.map(AccountsRouteResponse::Accounts)
.map(AccountsResponse::Account),
.map_output(AccountsRouteResponse::Accounts)
.map_output(AccountsResponse::Account),
AccountsRoute::AddAccount => {
AccountLoginView::new(login_state, app_ctx.clipboard, app_ctx.i18n)
let action = AccountLoginView::new(login_state, app_ctx.clipboard, app_ctx.i18n)
.ui(ui)
.inner
.map(AccountsRouteResponse::AddAccount)
.map(AccountsResponse::Account)
.map(AccountsResponse::Account);
BodyResponse::output(action)
}
AccountsRoute::Onboarding => FollowPackOnboardingView::new(
onboarding,
@@ -110,7 +111,7 @@ pub fn render_accounts_route(
jobs,
)
.ui(ui)
.map(|r| match r {
.map_output(|r| match r {
OnboardingResponse::FollowPacks(follow_packs_response) => {
AccountsResponse::Account(AccountsRouteResponse::AddAccount(
AccountLoginResponse::Onboarding(follow_packs_response),

View File

@@ -33,6 +33,7 @@ use crate::{
Damus,
};
use egui::scroll_area::ScrollAreaOutput;
use egui_nav::{Nav, NavAction, NavResponse, NavUiType, Percent, PopupResponse, PopupSheet};
use enostr::ProfileState;
use nostrdb::{Filter, Ndb, Transaction};
@@ -532,7 +533,7 @@ fn render_nav_body(
depth: usize,
col: usize,
inner_rect: egui::Rect,
) -> Option<RenderNavAction> {
) -> BodyResponse<RenderNavAction> {
let mut note_context = NoteContext {
ndb: ctx.ndb,
accounts: ctx.accounts,
@@ -555,7 +556,7 @@ fn render_nav_body(
.is_some_and(|ind| ind == col)
&& app.options.contains(AppOptions::ScrollToTop);
let nav_action = render_timeline_route(
let resp = render_timeline_route(
&mut app.timeline_cache,
kind,
col,
@@ -574,7 +575,7 @@ fn render_nav_body(
app.options.remove(AppOptions::ScrollToTop);
}
nav_action
resp
}
Route::Thread(selection) => render_thread_route(
&mut app.threads,
@@ -585,8 +586,8 @@ fn render_nav_body(
&mut note_context,
&mut app.jobs,
),
Route::Accounts(amr) => 's: {
let Some(action) = render_accounts_route(
Route::Accounts(amr) => {
let resp = render_accounts_route(
ui,
ctx,
&mut app.jobs,
@@ -594,11 +595,9 @@ fn render_nav_body(
&mut app.onboarding,
&mut app.view_state.follow_packs,
*amr,
) else {
break 's None;
};
);
match action {
resp.map_output_maybe(|action| match action {
AccountsResponse::ViewProfile(pubkey) => {
Some(RenderNavAction::NoteAction(NoteAction::Profile(pubkey)))
}
@@ -611,11 +610,11 @@ fn render_nav_body(
.accounts_action
.map(|f| RenderNavAction::SwitchingAction(SwitchingAction::Accounts(f)))
}
}
})
}
Route::Relays => RelayView::new(ctx.pool, &mut app.view_state.id_string_map, ctx.i18n)
.ui(ui)
.map(RenderNavAction::RelayAction),
.map_output(RenderNavAction::RelayAction),
Route::Settings => SettingsView::new(
ctx.settings.get_settings_mut(),
@@ -624,7 +623,7 @@ fn render_nav_body(
&mut app.jobs,
)
.ui(ui)
.map(RenderNavAction::SettingsAction),
.map_output(RenderNavAction::SettingsAction),
Route::Reply(id) => {
let txn = if let Ok(txn) = Transaction::new(ctx.ndb) {
@@ -635,7 +634,7 @@ fn render_nav_body(
"Reply to unknown note",
"Error message when reply note cannot be found"
));
return None;
return BodyResponse::none();
};
let note = if let Ok(note) = ctx.ndb.get_note_by_id(&txn, id.bytes()) {
@@ -646,18 +645,20 @@ fn render_nav_body(
"Reply to unknown note",
"Error message when reply note cannot be found"
));
return None;
return BodyResponse::none();
};
let poster = ctx.accounts.selected_filled()?;
let Some(poster) = ctx.accounts.selected_filled() else {
return BodyResponse::none();
};
let action = {
let resp = {
let draft = app.drafts.reply_mut(note.id());
let mut options = app.note_options;
options.set(NoteOptions::Wide, false);
let response = ui::PostReplyView::new(
ui::PostReplyView::new(
&mut note_context,
poster,
draft,
@@ -667,12 +668,10 @@ fn render_nav_body(
&mut app.jobs,
col,
)
.show(ui);
response.action
.show(ui)
};
action.map(Into::into)
resp.map_output_maybe(|o| Some(o.action?.into()))
}
Route::Quote(id) => {
let txn = Transaction::new(ctx.ndb).expect("txn");
@@ -685,10 +684,13 @@ fn render_nav_body(
"Quote of unknown note",
"Error message when quote note cannot be found"
));
return None;
return BodyResponse::none();
};
let Some(poster) = ctx.accounts.selected_filled() else {
return BodyResponse::none();
};
let poster = ctx.accounts.selected_filled()?;
let draft = app.drafts.quote_mut(note.id());
let response = crate::ui::note::QuoteRepostView::new(
@@ -703,10 +705,12 @@ fn render_nav_body(
)
.show(ui);
response.action.map(Into::into)
response.map_output_maybe(|o| Some(o.action?.into()))
}
Route::ComposeNote => {
let kp = ctx.accounts.get_selected_account().key.to_full()?;
let Some(kp) = ctx.accounts.get_selected_account().key.to_full() else {
return BodyResponse::none();
};
let draft = app.drafts.compose_mut();
let txn = Transaction::new(ctx.ndb).expect("txn");
@@ -721,16 +725,16 @@ fn render_nav_body(
)
.ui(&txn, ui);
post_response.action.map(Into::into)
post_response.map_output_maybe(|o| Some(o.action?.into()))
}
Route::AddColumn(route) => {
render_add_column_routes(ui, app, ctx, col, route);
None
BodyResponse::none()
}
Route::Support => {
SupportView::new(&mut app.support, ctx.i18n).show(ui);
None
BodyResponse::none()
}
Route::Search => {
let id = ui.id().with(("search", depth, col));
@@ -759,7 +763,7 @@ fn render_nav_body(
&mut app.jobs,
)
.show(ui)
.map(RenderNavAction::NoteAction)
.map_output(RenderNavAction::NoteAction)
}
Route::NewDeck => {
let id = ui.id().with("new-deck");
@@ -784,7 +788,8 @@ fn render_nav_body(
.get_selected_router()
.go_back();
}
resp
BodyResponse::output(resp)
}
Route::EditDeck(index) => {
let mut action = None;
@@ -816,31 +821,37 @@ fn render_nav_body(
.go_back();
}
action
BodyResponse::output(action)
}
Route::EditProfile(pubkey) => {
let mut action = None;
let Some(kp) = ctx.accounts.get_full(pubkey) else {
error!("Pubkey in EditProfile route did not have an nsec attached in Accounts");
return None;
return BodyResponse::none();
};
let Some(state) = app.view_state.pubkey_to_profile_state.get_mut(kp.pubkey) else {
tracing::error!(
"No profile state when navigating to EditProfile... was handle_navigating_edit_profile not called?"
);
return action;
return BodyResponse::none();
};
if EditProfileView::new(ctx.i18n, state, ctx.img_cache, ctx.clipboard).ui(ui) {
if let Some(state) = app.view_state.pubkey_to_profile_state.get(kp.pubkey) {
action = Some(RenderNavAction::ProfileAction(ProfileAction::SaveChanges(
EditProfileView::new(ctx.i18n, state, ctx.img_cache, ctx.clipboard)
.ui(ui)
.map_output_maybe(|save| {
if save {
app.view_state
.pubkey_to_profile_state
.get(kp.pubkey)
.map(|state| {
RenderNavAction::ProfileAction(ProfileAction::SaveChanges(
SaveProfileChanges::new(kp.to_full(), state.clone()),
)))
))
})
} else {
None
}
}
action
})
}
Route::Wallet(wallet_type) => {
let state = match wallet_type {
@@ -887,13 +898,13 @@ fn render_nav_body(
}
};
WalletView::new(state, ctx.i18n, ctx.clipboard)
.ui(ui)
.map(RenderNavAction::WalletAction)
BodyResponse::output(WalletView::new(state, ctx.i18n, ctx.clipboard).ui(ui))
.map_output(RenderNavAction::WalletAction)
}
Route::CustomizeZapAmount(target) => {
let txn = Transaction::new(ctx.ndb).expect("txn");
let default_msats = get_current_default_msats(ctx.accounts, ctx.global_wallet);
BodyResponse::output(
CustomZapView::new(
ctx.i18n,
ctx.img_cache,
@@ -902,8 +913,9 @@ fn render_nav_body(
&target.zap_recipient,
default_msats,
)
.ui(ui)
.map(|msats| {
.ui(ui),
)
.map_output(|msats| {
get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
.column_mut(col)
.router_mut()
@@ -919,6 +931,87 @@ fn render_nav_body(
}
}
pub struct BodyResponse<R> {
pub drag_id: Option<egui::Id>, // the id which was used for dragging.
pub output: Option<R>,
}
impl<R> BodyResponse<R> {
pub fn none() -> Self {
Self {
drag_id: None,
output: None,
}
}
pub fn scroll(output: ScrollAreaOutput<Option<R>>) -> Self {
Self {
drag_id: Some(Self::scroll_output_to_drag_id(output.id)),
output: output.inner,
}
}
pub fn set_scroll_id(&mut self, output: &ScrollAreaOutput<Option<R>>) {
self.drag_id = Some(Self::scroll_output_to_drag_id(output.id));
}
pub fn output(output: Option<R>) -> Self {
Self {
drag_id: None,
output,
}
}
pub fn set_output(&mut self, output: R) {
self.output = Some(output);
}
/// The id of an `egui::ScrollAreaOutput`
/// Should use `Self::scroll` when possible
pub fn scroll_raw(mut self, id: egui::Id) -> Self {
self.drag_id = Some(Self::scroll_output_to_drag_id(id));
self
}
/// The id which is directly used for dragging
pub fn set_drag_id_raw(&mut self, id: egui::Id) {
self.drag_id = Some(id);
}
fn scroll_output_to_drag_id(id: egui::Id) -> egui::Id {
id.with("area")
}
pub fn map_output<S>(self, f: impl FnOnce(R) -> S) -> BodyResponse<S> {
BodyResponse {
drag_id: self.drag_id,
output: self.output.map(f),
}
}
pub fn map_output_maybe<S>(self, f: impl FnOnce(R) -> Option<S>) -> BodyResponse<S> {
BodyResponse {
drag_id: self.drag_id,
output: self.output.and_then(f),
}
}
pub fn maybe_map_output<S>(self, f: impl FnOnce(Option<R>) -> S) -> BodyResponse<S> {
BodyResponse {
drag_id: self.drag_id,
output: Some(f(self.output)),
}
}
/// insert the contents of the new BodyResponse if they are empty in Self
pub fn insert(&mut self, body: BodyResponse<R>) {
self.drag_id = self.drag_id.or(body.drag_id);
if self.output.is_none() {
self.output = body.output;
}
}
}
#[must_use = "RenderNavResponse must be handled by calling .process_render_nav_response(..)"]
pub fn render_nav(
col: usize,
@@ -967,7 +1060,9 @@ pub fn render_nav(
.show_move_button(!narrow)
.show_delete_button(!narrow)
.show(ui),
NavUiType::Body => render_nav_body(ui, app, ctx, route, 1, col, inner_rect),
NavUiType::Body => {
render_nav_body(ui, app, ctx, route, 1, col, inner_rect).output
}
});
return RenderNavResponse::new(col, NotedeckNavResponse::Popup(Box::new(resp)));
@@ -1047,8 +1142,9 @@ pub fn render_nav(
if let Some(top) = nav.routes().last() {
render_nav_body(ui, app, ctx, top, nav.routes().len(), col, inner_rect)
} else {
None
BodyResponse::none()
}
.output
}
});

View File

@@ -1,5 +1,5 @@
use crate::{
nav::RenderNavAction,
nav::{BodyResponse, RenderNavAction},
profile::ProfileAction,
timeline::{thread::Threads, ThreadSelection, TimelineCache, TimelineKind},
ui::{self, ProfileView},
@@ -20,7 +20,7 @@ pub fn render_timeline_route(
note_context: &mut NoteContext,
jobs: &mut JobsCache,
scroll_to_top: bool,
) -> Option<RenderNavAction> {
) -> BodyResponse<RenderNavAction> {
match kind {
TimelineKind::List(_)
| TimelineKind::Search(_)
@@ -29,11 +29,11 @@ pub fn render_timeline_route(
| TimelineKind::Universe
| TimelineKind::Hashtag(_)
| TimelineKind::Generic(_) => {
let note_action =
let resp =
ui::TimelineView::new(kind, timeline_cache, note_context, note_options, jobs, col)
.ui(ui);
note_action.map(RenderNavAction::NoteAction)
resp.map_output(RenderNavAction::NoteAction)
}
TimelineKind::Profile(pubkey) => {
@@ -49,7 +49,7 @@ pub fn render_timeline_route(
)
} else {
// we render profiles like timelines if they are at the root
let note_action = ui::TimelineView::new(
let resp = ui::TimelineView::new(
kind,
timeline_cache,
note_context,
@@ -60,7 +60,7 @@ pub fn render_timeline_route(
.scroll_to_top(scroll_to_top)
.ui(ui);
note_action.map(RenderNavAction::NoteAction)
resp.map_output(RenderNavAction::NoteAction)
}
}
}
@@ -75,7 +75,7 @@ pub fn render_thread_route(
ui: &mut egui::Ui,
note_context: &mut NoteContext,
jobs: &mut JobsCache,
) -> Option<RenderNavAction> {
) -> BodyResponse<RenderNavAction> {
// don't truncate thread notes for now, since they are
// default truncated everywher eelse
note_options.set(NoteOptions::Truncate, false);
@@ -92,7 +92,7 @@ pub fn render_thread_route(
col,
)
.ui(ui)
.map(Into::into)
.map_output(RenderNavAction::NoteAction)
}
#[allow(clippy::too_many_arguments)]
@@ -104,7 +104,7 @@ pub fn render_profile_route(
note_options: NoteOptions,
note_context: &mut NoteContext,
jobs: &mut JobsCache,
) -> Option<RenderNavAction> {
) -> BodyResponse<RenderNavAction> {
let profile_view = ProfileView::new(
pubkey,
col,
@@ -115,8 +115,7 @@ pub fn render_profile_route(
)
.ui(ui);
if let Some(action) = profile_view {
match action {
profile_view.map_output_maybe(|action| match action {
ui::profile::ProfileViewAction::EditProfile => note_context
.accounts
.get_full(pubkey)
@@ -124,14 +123,11 @@ pub fn render_profile_route(
ui::profile::ProfileViewAction::Note(note_action) => {
Some(RenderNavAction::NoteAction(note_action))
}
ui::profile::ProfileViewAction::Follow(target_key) => Some(
RenderNavAction::ProfileAction(ProfileAction::Follow(target_key)),
),
ui::profile::ProfileViewAction::Follow(target_key) => Some(RenderNavAction::ProfileAction(
ProfileAction::Follow(target_key),
)),
ui::profile::ProfileViewAction::Unfollow(target_key) => Some(
RenderNavAction::ProfileAction(ProfileAction::Unfollow(target_key)),
),
}
} else {
None
}
})
}

View File

@@ -9,6 +9,8 @@ use notedeck_ui::profile::preview::SimpleProfilePreview;
use notedeck_ui::app_images;
use crate::nav::BodyResponse;
pub struct AccountsView<'a> {
ndb: &'a Ndb,
accounts: &'a Accounts,
@@ -44,20 +46,26 @@ impl<'a> AccountsView<'a> {
}
}
pub fn ui(&mut self, ui: &mut Ui) -> InnerResponse<Option<AccountsViewResponse>> {
pub fn ui(&mut self, ui: &mut Ui) -> BodyResponse<AccountsViewResponse> {
let mut out = BodyResponse::none();
Frame::new().outer_margin(12.0).show(ui, |ui| {
if let Some(resp) = Self::top_section_buttons_widget(ui, self.i18n).inner {
return Some(resp);
out.set_output(resp);
}
ui.add_space(8.0);
scroll_area()
let scroll_out = scroll_area()
.id_salt(AccountsView::scroll_id())
.show(ui, |ui| {
Self::show_accounts(ui, self.accounts, self.ndb, self.img_cache, self.i18n)
})
.inner
})
});
out.set_scroll_id(&scroll_out);
if let Some(scroll_output) = scroll_out.inner {
out.set_output(scroll_output);
}
});
out
}
pub fn scroll_id() -> egui::Id {

View File

@@ -11,6 +11,8 @@ use notedeck_ui::{
};
use tracing::error;
use crate::nav::BodyResponse;
/// Displays user profiles for the user to pick from.
/// Useful for manually typing a username and selecting the profile desired
pub struct MentionPickerView<'a> {
@@ -64,7 +66,11 @@ impl<'a> MentionPickerView<'a> {
MentionPickerResponse::SelectResult(selection)
}
pub fn show_in_rect(&mut self, rect: egui::Rect, ui: &mut egui::Ui) -> MentionPickerResponse {
pub fn show_in_rect(
&mut self,
rect: egui::Rect,
ui: &mut egui::Ui,
) -> BodyResponse<MentionPickerResponse> {
let widget_id = ui.id().with("mention_results");
let area_resp = egui::Area::new(widget_id)
.order(egui::Order::Foreground)
@@ -102,15 +108,17 @@ impl<'a> MentionPickerView<'a> {
let scroll_resp = ScrollArea::vertical()
.max_width(rect.width())
.auto_shrink(Vec2b::FALSE)
.show(ui, |ui| self.show(ui, width));
.show(ui, |ui| Some(self.show(ui, width)));
ui.advance_cursor_after_rect(rect);
BodyResponse::scroll(scroll_resp).map_output(|o| {
if close_button_resp {
MentionPickerResponse::DeleteMention
} else {
scroll_resp.inner
o
}
})
})
.inner
});

View File

@@ -1,5 +1,6 @@
use crate::draft::{Draft, Drafts, MentionHint};
use crate::media_upload::nostrbuild_nip96_upload;
use crate::nav::BodyResponse;
use crate::post::{downcast_post_buffer, MentionType, NewPost};
use crate::ui::mentions_picker::MentionPickerView;
use crate::ui::{self, Preview, PreviewConfig};
@@ -143,7 +144,7 @@ impl<'a, 'd> PostView<'a, 'd> {
self
}
fn editbox(&mut self, txn: &nostrdb::Transaction, ui: &mut egui::Ui) -> egui::Response {
fn editbox(&mut self, txn: &nostrdb::Transaction, ui: &mut egui::Ui) -> EditBoxResponse {
ui.spacing_mut().item_spacing.x = 12.0;
let pfp_size = 24.0;
@@ -221,37 +222,42 @@ impl<'a, 'd> PostView<'a, 'd> {
self.draft.buffer.selected_mention = false;
}
let mention_hints_drag_id =
if let Some(cursor_index) = get_cursor_index(&out.state.cursor.char_range()) {
self.show_mention_hints(txn, ui, cursor_index, &out);
}
self.show_mention_hints(txn, ui, cursor_index, &out)
} else {
None
};
let focused = out.response.has_focus();
ui.ctx()
.data_mut(|d| d.insert_temp(PostView::id(), focused));
out.response
EditBoxResponse {
resp: out.response,
mention_hints_drag_id,
}
}
// Displays the mention picker and handles when one is selected.
// returns the drag id of the mention hint widget
fn show_mention_hints(
&mut self,
txn: &nostrdb::Transaction,
ui: &mut egui::Ui,
cursor_index: usize,
textedit_output: &TextEditOutput,
) {
let Some(mention) = self.draft.buffer.get_mention(cursor_index) else {
return;
};
) -> Option<egui::Id> {
let mention = self.draft.buffer.get_mention(cursor_index)?;
if mention.info.mention_type != MentionType::Pending {
return;
return None;
}
if ui.ctx().input(|r| r.key_pressed(egui::Key::Escape)) {
self.draft.buffer.delete_mention(mention.index);
return;
return None;
}
let mention_str = self.draft.buffer.get_mention_string(&mention);
@@ -274,10 +280,8 @@ impl<'a, 'd> PostView<'a, 'd> {
}
let hint_rect = {
let hint = if let Some(hint) = &self.draft.cur_mention_hint {
hint
} else {
return;
let Some(hint) = &self.draft.cur_mention_hint else {
return None;
};
let mut hint_rect = self.inner_rect;
@@ -285,9 +289,11 @@ impl<'a, 'd> PostView<'a, 'd> {
hint_rect
};
let Ok(res) = self.note_context.ndb.search_profile(txn, mention_str, 10) else {
return;
};
let res = self
.note_context
.ndb
.search_profile(txn, mention_str, 10)
.ok()?;
let resp = MentionPickerView::new(
self.note_context.img_cache,
@@ -298,7 +304,12 @@ impl<'a, 'd> PostView<'a, 'd> {
.show_in_rect(hint_rect, ui);
let mut selection_made = None;
match resp {
let Some(out) = resp.output else {
return resp.drag_id;
};
match out {
ui::mentions_picker::MentionPickerResponse::SelectResult(selection) => {
if let Some(hint_index) = selection {
if let Some(pk) = res.get(hint_index) {
@@ -326,6 +337,8 @@ impl<'a, 'd> PostView<'a, 'd> {
if let Some(selection) = selection_made {
selection.process(ui.ctx(), textedit_output);
}
resp.drag_id
}
fn focused(&self, ui: &egui::Ui) -> bool {
@@ -341,14 +354,25 @@ impl<'a, 'd> PostView<'a, 'd> {
12
}
pub fn ui(&mut self, txn: &Transaction, ui: &mut egui::Ui) -> PostResponse {
ScrollArea::vertical()
pub fn ui(&mut self, txn: &Transaction, ui: &mut egui::Ui) -> BodyResponse<PostResponse> {
let scroll_out = ScrollArea::vertical()
.id_salt(PostView::scroll_id())
.show(ui, |ui| self.ui_no_scroll(txn, ui))
.inner
.show(ui, |ui| Some(self.ui_no_scroll(txn, ui)));
let scroll_id = scroll_out.id;
if let Some(inner) = scroll_out.inner {
inner // should override the PostView scroll for the mention scroll
} else {
BodyResponse::none()
}
.scroll_raw(scroll_id)
}
pub fn ui_no_scroll(&mut self, txn: &Transaction, ui: &mut egui::Ui) -> PostResponse {
pub fn ui_no_scroll(
&mut self,
txn: &Transaction,
ui: &mut egui::Ui,
) -> BodyResponse<PostResponse> {
while let Some(selected_file) = get_next_selected_file() {
match selected_file {
Ok(selected_media) => {
@@ -393,7 +417,7 @@ impl<'a, 'd> PostView<'a, 'd> {
.inner
}
fn input_ui(&mut self, txn: &Transaction, ui: &mut egui::Ui) -> PostResponse {
fn input_ui(&mut self, txn: &Transaction, ui: &mut egui::Ui) -> BodyResponse<PostResponse> {
let edit_response = ui.horizontal(|ui| self.editbox(txn, ui)).inner;
let note_response = if let PostType::Quote(id) = self.post_type {
@@ -445,10 +469,14 @@ impl<'a, 'd> PostView<'a, 'd> {
.and_then(|nr| nr.action.map(PostAction::QuotedNoteAction))
.or(post_action.map(PostAction::NewPostAction));
PostResponse {
action,
edit_response,
let mut resp = BodyResponse::output(action);
if let Some(drag_id) = edit_response.mention_hints_drag_id {
resp.set_drag_id_raw(drag_id);
}
resp.maybe_map_output(|action| PostResponse {
action,
edit_response: edit_response.resp,
})
}
fn input_buttons(&mut self, ui: &mut egui::Ui) -> Option<NewPostAction> {
@@ -596,6 +624,11 @@ impl<'a, 'd> PostView<'a, 'd> {
}
}
struct EditBoxResponse {
resp: egui::Response,
mention_hints_drag_id: Option<egui::Id>,
}
#[allow(clippy::too_many_arguments)]
fn render_post_view_media(
ui: &mut egui::Ui,

View File

@@ -1,6 +1,7 @@
use super::{PostResponse, PostType};
use crate::{
draft::Draft,
nav::BodyResponse,
ui::{self},
};
@@ -52,14 +53,22 @@ impl<'a, 'd> QuoteRepostView<'a, 'd> {
QuoteRepostView::id(col, note_id).with("scroll")
}
pub fn show(&mut self, ui: &mut egui::Ui) -> PostResponse {
ScrollArea::vertical()
pub fn show(&mut self, ui: &mut egui::Ui) -> BodyResponse<PostResponse> {
let scroll_out = ScrollArea::vertical()
.id_salt(self.scroll_id)
.show(ui, |ui| self.show_internal(ui))
.inner
.show(ui, |ui| Some(self.show_internal(ui)));
let scroll_id = scroll_out.id;
if let Some(inner) = scroll_out.inner {
inner
} else {
BodyResponse::none()
}
.scroll_raw(scroll_id)
}
fn show_internal(&mut self, ui: &mut egui::Ui) -> PostResponse {
fn show_internal(&mut self, ui: &mut egui::Ui) -> BodyResponse<PostResponse> {
let quoting_note_id = self.quoting_note.id();
let post_resp = ui::PostView::new(

View File

@@ -1,4 +1,5 @@
use crate::draft::Draft;
use crate::nav::BodyResponse;
use crate::ui::{
self,
note::{PostAction, PostResponse, PostType},
@@ -52,16 +53,23 @@ impl<'a, 'd> PostReplyView<'a, 'd> {
PostReplyView::id(col, note_id).with("scroll")
}
pub fn show(&mut self, ui: &mut egui::Ui) -> PostResponse {
ScrollArea::vertical()
pub fn show(&mut self, ui: &mut egui::Ui) -> BodyResponse<PostResponse> {
let scroll_out = ScrollArea::vertical()
.id_salt(self.scroll_id)
.stick_to_bottom(true)
.show(ui, |ui| self.show_internal(ui))
.inner
.show(ui, |ui| Some(self.show_internal(ui)));
let scroll_id = scroll_out.id;
if let Some(inner) = scroll_out.inner {
inner
} else {
BodyResponse::none()
}
.scroll_raw(scroll_id)
}
// no scroll
fn show_internal(&mut self, ui: &mut egui::Ui) -> PostResponse {
fn show_internal(&mut self, ui: &mut egui::Ui) -> BodyResponse<PostResponse> {
ui.vertical(|ui| {
let avail_rect = ui.available_rect_before_wrap();
@@ -103,17 +111,22 @@ impl<'a, 'd> PostReplyView<'a, 'd> {
.ui_no_scroll(self.note.txn().unwrap(), ui)
};
post_response.action = post_response
post_response = post_response.map_output(|mut o| {
o.action = o
.action
.or(quoted_note.action.map(PostAction::QuotedNoteAction));
o
});
if let Some(p) = &post_response.output {
reply_line_ui(
&rect_before_post,
&post_response.edit_response,
&p.edit_response,
pfp_offset as f32,
&avail_rect,
ui,
);
}
//
// NOTE(jb55): We add some space so that you can scroll to

View File

@@ -8,7 +8,7 @@ use notedeck_ui::{
nip51_set::{Nip51SetUiCache, Nip51SetWidget, Nip51SetWidgetAction, Nip51SetWidgetFlags},
};
use crate::{onboarding::Onboarding, ui::widgets::styled_button};
use crate::{nav::BodyResponse, onboarding::Onboarding, ui::widgets::styled_button};
/// Display Follow Packs for the user to choose from authors trusted by the Damus team
pub struct FollowPackOnboardingView<'a> {
@@ -56,17 +56,17 @@ impl<'a> FollowPackOnboardingView<'a> {
egui::Id::new("follow_pack_onboarding")
}
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<OnboardingResponse> {
pub fn ui(&mut self, ui: &mut egui::Ui) -> BodyResponse<OnboardingResponse> {
let Some(follow_pack_state) = self.onboarding.get_follow_packs() else {
return Some(OnboardingResponse::FollowPacks(
return BodyResponse::output(Some(OnboardingResponse::FollowPacks(
FollowPacksResponse::NoFollowPacks,
));
)));
};
let max_height = ui.available_height() - 48.0;
let mut action = None;
ScrollArea::vertical()
let scroll_out = ScrollArea::vertical()
.id_salt(Self::scroll_id())
.max_height(max_height)
.show(ui, |ui| {
@@ -114,6 +114,6 @@ impl<'a> FollowPackOnboardingView<'a> {
}
});
action
BodyResponse::output(action).scroll_raw(scroll_out.id)
}
}

View File

@@ -7,6 +7,8 @@ use notedeck::{profile::unwrap_profile_url, tr, Images, Localization, NotedeckTe
use notedeck_ui::context_menu::{input_context, PasteBehavior};
use notedeck_ui::{profile::banner, ProfilePic};
use crate::nav::BodyResponse;
pub struct EditProfileView<'a> {
state: &'a mut ProfileState,
clipboard: &'a mut Clipboard,
@@ -34,8 +36,8 @@ impl<'a> EditProfileView<'a> {
}
// return true to save
pub fn ui(&mut self, ui: &mut egui::Ui) -> bool {
ScrollArea::vertical()
pub fn ui(&mut self, ui: &mut egui::Ui) -> BodyResponse<bool> {
let scroll_out = ScrollArea::vertical()
.id_salt(EditProfileView::scroll_id())
.stick_to_bottom(true)
.show(ui, |ui| {
@@ -71,9 +73,9 @@ impl<'a> EditProfileView<'a> {
});
});
save
})
.inner
Some(save)
});
BodyResponse::scroll(scroll_out)
}
fn inner(&mut self, ui: &mut egui::Ui, padding: f32) {

View File

@@ -10,6 +10,7 @@ use robius_open::Uri;
use tracing::error;
use crate::{
nav::BodyResponse,
timeline::{TimelineCache, TimelineKind},
ui::timeline::{tabs_ui, TimelineTabView},
};
@@ -68,13 +69,16 @@ impl<'a, 'd> ProfileView<'a, 'd> {
egui::Id::new(("profile_scroll", col_id, profile_pubkey))
}
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<ProfileViewAction> {
pub fn ui(&mut self, ui: &mut egui::Ui) -> BodyResponse<ProfileViewAction> {
let scroll_id = ProfileView::scroll_id(self.col_id, self.pubkey);
let scroll_area = ScrollArea::vertical().id_salt(scroll_id).animated(false);
let profile_timeline = self
let Some(profile_timeline) = self
.timeline_cache
.get_mut(&TimelineKind::Profile(*self.pubkey))?;
.get_mut(&TimelineKind::Profile(*self.pubkey))
else {
return BodyResponse::none();
};
let output = scroll_area.show(ui, |ui| {
let mut action = None;
@@ -132,7 +136,7 @@ impl<'a, 'd> ProfileView<'a, 'd> {
// only allow front insert when the profile body is fully obstructed
profile_timeline.enable_front_insert = output.inner.body_end_pos < ui.clip_rect().top();
output.inner.action
BodyResponse::output(output.inner.action).scroll_raw(output.id)
}
}

View File

@@ -1,5 +1,6 @@
use std::collections::HashMap;
use crate::nav::BodyResponse;
use crate::ui::{Preview, PreviewConfig};
use egui::{Align, Button, CornerRadius, Frame, Id, Layout, Margin, Rgba, RichText, Ui, Vec2};
use enostr::{RelayPool, RelayStatus};
@@ -17,9 +18,8 @@ pub struct RelayView<'a> {
}
impl RelayView<'_> {
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<RelayAction> {
let mut action = None;
Frame::new()
pub fn ui(&mut self, ui: &mut egui::Ui) -> BodyResponse<RelayAction> {
let scroll_out = Frame::new()
.inner_margin(Margin::symmetric(10, 0))
.show(ui, |ui| {
ui.add_space(24.0);
@@ -40,6 +40,7 @@ impl RelayView<'_> {
.scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false; 2])
.show(ui, |ui| {
let mut action = None;
if let Some(relay_to_remove) = self.show_relays(ui) {
action = Some(RelayAction::Remove(relay_to_remove));
}
@@ -47,10 +48,12 @@ impl RelayView<'_> {
if let Some(relay_to_add) = self.show_add_relay_ui(ui) {
action = Some(RelayAction::Add(relay_to_add));
}
});
});
action
})
})
.inner;
BodyResponse::scroll(scroll_out)
}
pub fn scroll_id() -> egui::Id {

View File

@@ -3,6 +3,7 @@ use enostr::{NoteId, Pubkey};
use state::TypingType;
use crate::{
nav::BodyResponse,
timeline::{TimelineTab, TimelineUnits},
ui::timeline::TimelineTabView,
};
@@ -49,11 +50,11 @@ impl<'a, 'd> SearchView<'a, 'd> {
}
}
pub fn show(&mut self, ui: &mut egui::Ui) -> Option<NoteAction> {
pub fn show(&mut self, ui: &mut egui::Ui) -> BodyResponse<NoteAction> {
padding(8.0, ui, |ui| self.show_impl(ui)).inner
}
pub fn show_impl(&mut self, ui: &mut egui::Ui) -> Option<NoteAction> {
pub fn show_impl(&mut self, ui: &mut egui::Ui) -> BodyResponse<NoteAction> {
ui.spacing_mut().item_spacing = egui::vec2(0.0, 12.0);
let search_resp = search_box(
@@ -67,7 +68,7 @@ impl<'a, 'd> SearchView<'a, 'd> {
search_resp.process(self.query);
let mut search_action = None;
let mut note_action = None;
let mut body_resp = BodyResponse::none();
match &self.query.state {
SearchState::New | SearchState::Navigating => {}
SearchState::Typing(TypingType::Mention(mention_name)) => 's: {
@@ -87,7 +88,11 @@ impl<'a, 'd> SearchView<'a, 'd> {
)
.show_in_rect(ui.available_rect_before_wrap(), ui);
search_action = match search_res {
let Some(res) = search_res.output else {
break 's;
};
search_action = match res {
MentionPickerResponse::SelectResult(Some(index)) => {
let Some(pk_bytes) = results.get(index) else {
break 's;
@@ -120,7 +125,7 @@ impl<'a, 'd> SearchView<'a, 'd> {
&mut self.query.notes,
);
search_action = Some(SearchAction::Searched);
note_action = self.show_search_results(ui);
body_resp.insert(self.show_search_results(ui));
}
SearchState::Searched => {
ui.label(tr_plural!(
@@ -131,7 +136,7 @@ impl<'a, 'd> SearchView<'a, 'd> {
self.query.notes.units.len(), // count
query = &self.query.string
));
note_action = self.show_search_results(ui);
body_resp.insert(self.show_search_results(ui));
}
SearchState::Typing(TypingType::AutoSearch) => {
ui.label(tr!(
@@ -141,7 +146,7 @@ impl<'a, 'd> SearchView<'a, 'd> {
query = &self.query.string
));
note_action = self.show_search_results(ui);
body_resp.insert(self.show_search_results(ui));
}
};
@@ -149,11 +154,11 @@ impl<'a, 'd> SearchView<'a, 'd> {
resp.process(self.query);
}
note_action
body_resp
}
fn show_search_results(&mut self, ui: &mut egui::Ui) -> Option<NoteAction> {
egui::ScrollArea::vertical()
fn show_search_results(&mut self, ui: &mut egui::Ui) -> BodyResponse<NoteAction> {
let scroll_out = egui::ScrollArea::vertical()
.id_salt(SearchView::scroll_id())
.show(ui, |ui| {
TimelineTabView::new(
@@ -164,8 +169,9 @@ impl<'a, 'd> SearchView<'a, 'd> {
self.jobs,
)
.show(ui)
})
.inner
});
BodyResponse::scroll(scroll_out)
}
pub fn scroll_id() -> egui::Id {

View File

@@ -16,7 +16,11 @@ use notedeck_ui::{
AnimationHelper, NoteOptions, NoteView,
};
use crate::{nav::RouterAction, ui::account_login_view::eye_button, Damus, Route};
use crate::{
nav::{BodyResponse, RouterAction},
ui::account_login_view::eye_button,
Damus, Route,
};
const PREVIEW_NOTE_ID: &str = "note1edjc8ggj07hwv77g2405uh6j2jkk5aud22gktxrvc2wnre4vdwgqzlv2gw";
@@ -638,13 +642,12 @@ impl<'a> SettingsView<'a> {
action
}
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> {
let mut action: Option<SettingsAction> = None;
Frame::default()
pub fn ui(&mut self, ui: &mut egui::Ui) -> BodyResponse<SettingsAction> {
let scroll_out = Frame::default()
.inner_margin(Margin::symmetric(10, 10))
.show(ui, |ui| {
ScrollArea::vertical().show(ui, |ui| {
let mut action = None;
if let Some(new_action) = self.appearance_section(ui) {
action = Some(new_action);
}
@@ -670,10 +673,12 @@ impl<'a> SettingsView<'a> {
if let Some(new_action) = self.manage_relays_section(ui) {
action = Some(new_action);
}
});
});
action
})
})
.inner;
BodyResponse::scroll(scroll_out)
}
}

View File

@@ -7,6 +7,7 @@ use notedeck::{NoteAction, NoteContext};
use notedeck_ui::note::NoteResponse;
use notedeck_ui::{NoteOptions, NoteView};
use crate::nav::BodyResponse;
use crate::timeline::thread::{NoteSeenFlags, ParentState, Threads};
pub struct ThreadView<'a, 'd> {
@@ -42,7 +43,7 @@ impl<'a, 'd> ThreadView<'a, 'd> {
egui::Id::new(("threadscroll", selected_note_id, col))
}
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<NoteAction> {
pub fn ui(&mut self, ui: &mut egui::Ui) -> BodyResponse<NoteAction> {
let txn = Transaction::new(self.note_context.ndb).expect("txn");
let scroll_id = ThreadView::scroll_id(self.selected_note_id, self.col);
@@ -60,6 +61,7 @@ impl<'a, 'd> ThreadView<'a, 'd> {
let output = scroll_area.show(ui, |ui| self.notes(ui, &txn));
let out_id = output.id;
let mut resp = output.inner;
if let Some(NoteAction::Note {
@@ -71,7 +73,7 @@ impl<'a, 'd> ThreadView<'a, 'd> {
*scroll_offset = output.state.offset.y;
}
resp
BodyResponse::output(resp).scroll_raw(out_id)
}
fn notes(&mut self, ui: &mut egui::Ui, txn: &Transaction) -> Option<NoteAction> {

View File

@@ -12,6 +12,7 @@ use notedeck_ui::{ProfilePic, ProfilePreview};
use std::f32::consts::PI;
use tracing::{error, warn};
use crate::nav::BodyResponse;
use crate::timeline::{
CompositeType, CompositeUnit, NoteUnit, ReactionUnit, RepostUnit, TimelineCache, TimelineKind,
TimelineTab, ViewFilter,
@@ -56,7 +57,7 @@ impl<'a, 'd> TimelineView<'a, 'd> {
}
}
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<NoteAction> {
pub fn ui(&mut self, ui: &mut egui::Ui) -> BodyResponse<NoteAction> {
timeline_ui(
ui,
self.timeline_id,
@@ -94,7 +95,7 @@ fn timeline_ui(
jobs: &mut JobsCache,
col: usize,
scroll_to_top: bool,
) -> Option<NoteAction> {
) -> BodyResponse<NoteAction> {
//padding(4.0, ui, |ui| ui.heading("Notifications"));
/*
let font_id = egui::TextStyle::Body.resolve(ui.style());
@@ -102,7 +103,9 @@ fn timeline_ui(
*/
let scroll_id = TimelineView::scroll_id(timeline_cache, timeline_id, col)?;
let Some(scroll_id) = TimelineView::scroll_id(timeline_cache, timeline_id, col) else {
return BodyResponse::none();
};
{
let timeline = if let Some(timeline) = timeline_cache.get_mut(timeline_id) {
@@ -111,7 +114,7 @@ fn timeline_ui(
error!("tried to render timeline in column, but timeline was missing");
// TODO (jb55): render error when timeline is missing?
// this shouldn't happen...
return None;
return BodyResponse::none();
};
timeline.selected_view = tabs_ui(
@@ -204,7 +207,9 @@ fn timeline_ui(
.data_mut(|d| d.insert_temp(show_top_button_id, true));
}
scroll_output.inner.or_else(|| {
let scroll_id = scroll_output.id;
let action = scroll_output.inner.or_else(|| {
// if we're scrolling, return that as a response. We need this
// for auto-closing the side menu
@@ -215,7 +220,9 @@ fn timeline_ui(
} else {
None
}
})
});
BodyResponse::output(action).scroll_raw(scroll_id)
}
fn goto_top_button(center: Pos2) -> impl egui::Widget {