chrome: collapsible side panel

This implements the initial logic that makes the side panel collapsible.

Since we don't have a proper hamburger control, we do the same thing we
do on iOS for now.
This commit is contained in:
William Casarin
2025-06-05 11:51:07 -07:00
parent 5cb0911d7e
commit e87b6f1905
7 changed files with 281 additions and 135 deletions

View File

@@ -19,6 +19,7 @@ use tracing::{error, info};
pub enum AppAction { pub enum AppAction {
Note(NoteAction), Note(NoteAction),
ToggleChrome,
} }
pub trait App { pub trait App {

View File

@@ -16,12 +16,22 @@ use notedeck_ui::{AnimationHelper, ProfilePic};
static ICON_WIDTH: f32 = 40.0; static ICON_WIDTH: f32 = 40.0;
pub static ICON_EXPANSION_MULTIPLE: f32 = 1.2; pub static ICON_EXPANSION_MULTIPLE: f32 = 1.2;
#[derive(Default)]
pub struct Chrome { pub struct Chrome {
active: i32, active: i32,
open: bool,
apps: Vec<NotedeckApp>, apps: Vec<NotedeckApp>,
} }
impl Default for Chrome {
fn default() -> Self {
Self {
active: 0,
open: true,
apps: vec![],
}
}
}
pub enum ChromePanelAction { pub enum ChromePanelAction {
Support, Support,
Settings, Settings,
@@ -85,6 +95,10 @@ impl Chrome {
Chrome::default() Chrome::default()
} }
pub fn toggle(&mut self) {
self.open = !self.open;
}
pub fn add_app(&mut self, app: NotedeckApp) { pub fn add_app(&mut self, app: NotedeckApp) {
self.apps.push(app); self.apps.push(app);
} }
@@ -132,8 +146,11 @@ impl Chrome {
let mut got_action: Option<ChromePanelAction> = None; let mut got_action: Option<ChromePanelAction> = None;
let side_panel_width: f32 = 70.0; let side_panel_width: f32 = 70.0;
let open_id = egui::Id::new("chrome_open");
let amt_open = ui.ctx().animate_bool(open_id, self.open) * side_panel_width;
StripBuilder::new(ui) StripBuilder::new(ui)
.size(Size::exact(side_panel_width)) // collapsible sidebar .size(Size::exact(amt_open)) // collapsible sidebar
.size(Size::remainder()) // the main app contents .size(Size::remainder()) // the main app contents
.clip(true) .clip(true)
.horizontal(|mut strip| { .horizontal(|mut strip| {
@@ -294,7 +311,7 @@ impl Chrome {
if ui.add(expand_side_panel_button()).clicked() { if ui.add(expand_side_panel_button()).clicked() {
//self.active = (self.active + 1) % (self.apps.len() as i32); //self.active = (self.active + 1) % (self.apps.len() as i32);
// TODO: collapse sidebar ? self.open = !self.open;
} }
ui.add_space(4.0); ui.add_space(4.0);
@@ -492,6 +509,10 @@ fn chrome_handle_app_action(
ui: &mut egui::Ui, ui: &mut egui::Ui,
) { ) {
match action { match action {
AppAction::ToggleChrome => {
chrome.toggle();
}
AppAction::Note(note_action) => { AppAction::Note(note_action) => {
chrome.switch_to_columns(); chrome.switch_to_columns();
let Some(columns) = chrome.get_columns() else { let Some(columns) = chrome.get_columns() else {

View File

@@ -3,7 +3,7 @@ use crate::{
column::Columns, column::Columns,
decks::{Decks, DecksCache, FALLBACK_PUBKEY}, decks::{Decks, DecksCache, FALLBACK_PUBKEY},
draft::Drafts, draft::Drafts,
nav, nav::{self, ProcessNavResult},
route::Route, route::Route,
storage, storage,
subscriptions::{SubKind, Subscriptions}, subscriptions::{SubKind, Subscriptions},
@@ -340,15 +340,21 @@ fn process_message(damus: &mut Damus, ctx: &mut AppContext<'_>, relay: &str, msg
} }
} }
fn render_damus(damus: &mut Damus, app_ctx: &mut AppContext<'_>, ui: &mut egui::Ui) { fn render_damus(
if notedeck::ui::is_narrow(ui.ctx()) { damus: &mut Damus,
render_damus_mobile(damus, app_ctx, ui); app_ctx: &mut AppContext<'_>,
ui: &mut egui::Ui,
) -> Option<AppAction> {
let app_action = if notedeck::ui::is_narrow(ui.ctx()) {
render_damus_mobile(damus, app_ctx, ui)
} else { } else {
render_damus_desktop(damus, app_ctx, ui); render_damus_desktop(damus, app_ctx, ui)
} };
// We use this for keeping timestamps and things up to date // We use this for keeping timestamps and things up to date
ui.ctx().request_repaint_after(Duration::from_secs(1)); ui.ctx().request_repaint_after(Duration::from_secs(1));
app_action
} }
/* /*
@@ -518,17 +524,32 @@ fn circle_icon(ui: &mut egui::Ui, openness: f32, response: &egui::Response) {
*/ */
#[profiling::function] #[profiling::function]
fn render_damus_mobile(app: &mut Damus, app_ctx: &mut AppContext<'_>, ui: &mut egui::Ui) { fn render_damus_mobile(
app: &mut Damus,
app_ctx: &mut AppContext<'_>,
ui: &mut egui::Ui,
) -> Option<AppAction> {
//let routes = app.timelines[0].routes.clone(); //let routes = app.timelines[0].routes.clone();
let mut rect = ui.available_rect_before_wrap(); let mut rect = ui.available_rect_before_wrap();
let mut app_action: Option<AppAction> = None;
if !app.columns(app_ctx.accounts).columns().is_empty() if !app.columns(app_ctx.accounts).columns().is_empty() {
&& nav::render_nav(0, ui.available_rect_before_wrap(), app, app_ctx, ui) let r = nav::render_nav(0, ui.available_rect_before_wrap(), app, app_ctx, ui)
.process_render_nav_response(app, app_ctx, ui) .process_render_nav_response(app, app_ctx, ui);
&& !app.tmp_columns if let Some(r) = &r {
{ match r {
storage::save_decks_cache(app_ctx.path, &app.decks_cache); ProcessNavResult::SwitchOccurred => {
if !app.tmp_columns {
storage::save_decks_cache(app_ctx.path, &app.decks_cache);
}
}
ProcessNavResult::PfpClicked => {
app_action = Some(AppAction::ToggleChrome);
}
}
}
} }
rect.min.x = rect.max.x - 100.0; rect.min.x = rect.max.x - 100.0;
@@ -549,10 +570,16 @@ fn render_damus_mobile(app: &mut Damus, app_ctx: &mut AppContext<'_>, ui: &mut e
router.route_to(Route::ComposeNote); router.route_to(Route::ComposeNote);
} }
} }
app_action
} }
#[profiling::function] #[profiling::function]
fn render_damus_desktop(app: &mut Damus, app_ctx: &mut AppContext<'_>, ui: &mut egui::Ui) { fn render_damus_desktop(
app: &mut Damus,
app_ctx: &mut AppContext<'_>,
ui: &mut egui::Ui,
) -> Option<AppAction> {
let screen_size = ui.ctx().screen_rect().width(); let screen_size = ui.ctx().screen_rect().width();
let calc_panel_width = (screen_size let calc_panel_width = (screen_size
/ get_active_columns(app_ctx.accounts, &app.decks_cache).num_columns() as f32) / get_active_columns(app_ctx.accounts, &app.decks_cache).num_columns() as f32)
@@ -566,16 +593,22 @@ fn render_damus_desktop(app: &mut Damus, app_ctx: &mut AppContext<'_>, ui: &mut
}; };
ui.spacing_mut().item_spacing.x = 0.0; ui.spacing_mut().item_spacing.x = 0.0;
if need_scroll { if need_scroll {
egui::ScrollArea::horizontal().show(ui, |ui| { egui::ScrollArea::horizontal()
timelines_view(ui, panel_sizes, app, app_ctx); .show(ui, |ui| timelines_view(ui, panel_sizes, app, app_ctx))
}); .inner
} else { } else {
timelines_view(ui, panel_sizes, app, app_ctx); timelines_view(ui, panel_sizes, app, app_ctx)
} }
} }
fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, ctx: &mut AppContext<'_>) { fn timelines_view(
ui: &mut egui::Ui,
sizes: Size,
app: &mut Damus,
ctx: &mut AppContext<'_>,
) -> Option<AppAction> {
let num_cols = get_active_columns(ctx.accounts, &app.decks_cache).num_columns(); let num_cols = get_active_columns(ctx.accounts, &app.decks_cache).num_columns();
let mut side_panel_action: Option<nav::SwitchingAction> = None; let mut side_panel_action: Option<nav::SwitchingAction> = None;
let mut responses = Vec::with_capacity(num_cols); let mut responses = Vec::with_capacity(num_cols);
@@ -654,9 +687,20 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, ctx: &mut App
save_cols = save_cols || action.process(&mut app.timeline_cache, &mut app.decks_cache, ctx); save_cols = save_cols || action.process(&mut app.timeline_cache, &mut app.decks_cache, ctx);
} }
let mut app_action: Option<AppAction> = None;
for response in responses { for response in responses {
let save = response.process_render_nav_response(app, ctx, ui); let nav_result = response.process_render_nav_response(app, ctx, ui);
save_cols = save_cols || save;
if let Some(nr) = &nav_result {
match nr {
ProcessNavResult::SwitchOccurred => save_cols = true,
ProcessNavResult::PfpClicked => {
app_action = Some(AppAction::ToggleChrome);
}
}
}
} }
if app.tmp_columns { if app.tmp_columns {
@@ -666,6 +710,8 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, ctx: &mut App
if save_cols { if save_cols {
storage::save_decks_cache(ctx.path, &app.decks_cache); storage::save_decks_cache(ctx.path, &app.decks_cache);
} }
app_action
} }
impl notedeck::App for Damus { impl notedeck::App for Damus {
@@ -677,9 +723,7 @@ impl notedeck::App for Damus {
*/ */
update_damus(self, ctx, ui.ctx()); update_damus(self, ctx, ui.ctx());
render_damus(self, ctx, ui); render_damus(self, ctx, ui)
None
} }
} }

View File

@@ -34,10 +34,24 @@ use notedeck::{
use notedeck_ui::View; use notedeck_ui::View;
use tracing::error; use tracing::error;
/// The result of processing a nav response
pub enum ProcessNavResult {
SwitchOccurred,
PfpClicked,
}
impl ProcessNavResult {
pub fn switch_occurred(&self) -> bool {
matches!(self, Self::SwitchOccurred)
}
}
#[allow(clippy::enum_variant_names)] #[allow(clippy::enum_variant_names)]
pub enum RenderNavAction { pub enum RenderNavAction {
Back, Back,
RemoveColumn, RemoveColumn,
/// The response when the user interacts with a pfp in the nav header
PfpClicked,
PostAction(NewPostAction), PostAction(NewPostAction),
NoteAction(NoteAction), NoteAction(NoteAction),
ProfileAction(ProfileAction), ProfileAction(ProfileAction),
@@ -144,11 +158,10 @@ impl RenderNavResponse {
app: &mut Damus, app: &mut Damus,
ctx: &mut AppContext<'_>, ctx: &mut AppContext<'_>,
ui: &mut egui::Ui, ui: &mut egui::Ui,
) -> bool { ) -> Option<ProcessNavResult> {
match self.response { match self.response {
NotedeckNavResponse::Popup(nav_action) => { NotedeckNavResponse::Popup(nav_action) => {
process_popup_resp(*nav_action, app, ctx, ui, self.column); process_popup_resp(*nav_action, app, ctx, ui, self.column)
false
} }
NotedeckNavResponse::Nav(nav_response) => { NotedeckNavResponse::Nav(nav_response) => {
process_nav_resp(app, ctx, ui, *nav_response, self.column) process_nav_resp(app, ctx, ui, *nav_response, self.column)
@@ -163,10 +176,10 @@ fn process_popup_resp(
ctx: &mut AppContext<'_>, ctx: &mut AppContext<'_>,
ui: &mut egui::Ui, ui: &mut egui::Ui,
col: usize, col: usize,
) -> bool { ) -> Option<ProcessNavResult> {
let mut switching_occured = false; let mut process_result: Option<ProcessNavResult> = None;
if let Some(nav_action) = action.response { if let Some(nav_action) = action.response {
switching_occured = process_render_nav_action(app, ctx, ui, col, nav_action); process_result = process_render_nav_action(app, ctx, ui, col, nav_action);
} }
if let Some(NavAction::Returned) = action.action { if let Some(NavAction::Returned) = action.action {
@@ -177,7 +190,7 @@ fn process_popup_resp(
column.sheet_router.navigating = false; column.sheet_router.navigating = false;
} }
switching_occured process_result
} }
fn process_nav_resp( fn process_nav_resp(
@@ -186,13 +199,13 @@ fn process_nav_resp(
ui: &mut egui::Ui, ui: &mut egui::Ui,
response: NavResponse<Option<RenderNavAction>>, response: NavResponse<Option<RenderNavAction>>,
col: usize, col: usize,
) -> bool { ) -> Option<ProcessNavResult> {
let mut switching_occured: bool = false; let mut process_result: Option<ProcessNavResult> = None;
if let Some(action) = response.response.or(response.title_response) { if let Some(action) = response.response.or(response.title_response) {
// start returning when we're finished posting // start returning when we're finished posting
switching_occured = process_render_nav_action(app, ctx, ui, col, action); process_result = process_render_nav_action(app, ctx, ui, col, action);
} }
if let Some(action) = response.action { if let Some(action) = response.action {
@@ -210,7 +223,7 @@ fn process_nav_resp(
} }
}; };
switching_occured = true; process_result = Some(ProcessNavResult::SwitchOccurred);
} }
NavAction::Navigated => { NavAction::Navigated => {
@@ -219,7 +232,8 @@ fn process_nav_resp(
if cur_router.is_replacing() { if cur_router.is_replacing() {
cur_router.remove_previous_routes(); cur_router.remove_previous_routes();
} }
switching_occured = true;
process_result = Some(ProcessNavResult::SwitchOccurred);
} }
NavAction::Dragging => {} NavAction::Dragging => {}
@@ -229,11 +243,15 @@ fn process_nav_resp(
} }
} }
switching_occured process_result
} }
pub enum RouterAction { pub enum RouterAction {
GoBack, GoBack,
/// We clicked on a pfp in a route. We currently don't carry any
/// information about the pfp since we only use it for toggling the
/// chrome atm
PfpClicked,
RouteTo(Route, RouterType), RouteTo(Route, RouterType),
} }
@@ -247,7 +265,7 @@ impl RouterAction {
self, self,
stack_router: &mut Router<Route>, stack_router: &mut Router<Route>,
sheet_router: &mut SingletonRouter<Route>, sheet_router: &mut SingletonRouter<Route>,
) { ) -> Option<ProcessNavResult> {
match self { match self {
RouterAction::GoBack => { RouterAction::GoBack => {
if sheet_router.route().is_some() { if sheet_router.route().is_some() {
@@ -255,10 +273,21 @@ impl RouterAction {
} else { } else {
stack_router.go_back(); stack_router.go_back();
} }
None
} }
RouterAction::PfpClicked => Some(ProcessNavResult::PfpClicked),
RouterAction::RouteTo(route, router_type) => match router_type { RouterAction::RouteTo(route, router_type) => match router_type {
RouterType::Sheet => sheet_router.route_to(route), RouterType::Sheet => {
RouterType::Stack => stack_router.route_to(route), sheet_router.route_to(route);
None
}
RouterType::Stack => {
stack_router.route_to(route);
None
}
}, },
} }
} }
@@ -278,9 +307,10 @@ fn process_render_nav_action(
ui: &mut egui::Ui, ui: &mut egui::Ui,
col: usize, col: usize,
action: RenderNavAction, action: RenderNavAction,
) -> bool { ) -> Option<ProcessNavResult> {
let router_action = match action { let router_action = match action {
RenderNavAction::Back => Some(RouterAction::GoBack), RenderNavAction::Back => Some(RouterAction::GoBack),
RenderNavAction::PfpClicked => Some(RouterAction::PfpClicked),
RenderNavAction::RemoveColumn => { RenderNavAction::RemoveColumn => {
let kinds_to_pop = app.columns_mut(ctx.accounts).delete_column(col); let kinds_to_pop = app.columns_mut(ctx.accounts).delete_column(col);
@@ -291,7 +321,7 @@ fn process_render_nav_action(
} }
} }
return true; return Some(ProcessNavResult::SwitchOccurred);
} }
RenderNavAction::PostAction(new_post_action) => { RenderNavAction::PostAction(new_post_action) => {
@@ -326,7 +356,11 @@ fn process_render_nav_action(
} }
RenderNavAction::SwitchingAction(switching_action) => { RenderNavAction::SwitchingAction(switching_action) => {
return switching_action.process(&mut app.timeline_cache, &mut app.decks_cache, ctx); if switching_action.process(&mut app.timeline_cache, &mut app.decks_cache, ctx) {
return Some(ProcessNavResult::SwitchOccurred);
} else {
return None;
}
} }
RenderNavAction::ProfileAction(profile_action) => profile_action.process( RenderNavAction::ProfileAction(profile_action) => profile_action.process(
&mut app.view_state.pubkey_to_profile_state, &mut app.view_state.pubkey_to_profile_state,
@@ -342,10 +376,11 @@ fn process_render_nav_action(
let cols = get_active_columns_mut(ctx.accounts, &mut app.decks_cache).column_mut(col); let cols = get_active_columns_mut(ctx.accounts, &mut app.decks_cache).column_mut(col);
let router = &mut cols.router; let router = &mut cols.router;
let sheet_router = &mut cols.sheet_router; let sheet_router = &mut cols.sheet_router;
action.process(router, sheet_router);
}
false action.process(router, sheet_router)
} else {
None
}
} }
fn render_nav_body( fn render_nav_body(

View File

@@ -8,8 +8,7 @@ use crate::{
ui::{self}, ui::{self},
}; };
use egui::Margin; use egui::{Margin, Response, RichText, Sense, Stroke, UiBuilder};
use egui::{RichText, Stroke, UiBuilder};
use enostr::Pubkey; use enostr::Pubkey;
use nostrdb::{Ndb, Transaction}; use nostrdb::{Ndb, Transaction};
use notedeck::{Images, NotedeckTextStyle}; use notedeck::{Images, NotedeckTextStyle};
@@ -84,8 +83,10 @@ impl<'a> NavTitle<'a> {
let title_resp = self.title(ui, self.routes.last().unwrap(), back_button_resp.is_some()); let title_resp = self.title(ui, self.routes.last().unwrap(), back_button_resp.is_some());
if let Some(resp) = title_resp { if let Some(resp) = title_resp {
tracing::debug!("got title response {resp:?}");
match resp { match resp {
TitleResponse::RemoveColumn => Some(RenderNavAction::RemoveColumn), TitleResponse::RemoveColumn => Some(RenderNavAction::RemoveColumn),
TitleResponse::PfpClicked => Some(RenderNavAction::PfpClicked),
TitleResponse::MoveColumn(to_index) => { TitleResponse::MoveColumn(to_index) => {
let from = self.col_id; let from = self.col_id;
Some(RenderNavAction::SwitchingAction(SwitchingAction::Columns( Some(RenderNavAction::SwitchingAction(SwitchingAction::Columns(
@@ -94,6 +95,7 @@ impl<'a> NavTitle<'a> {
} }
} }
} else if back_button_resp.is_some_and(|r| r.clicked()) { } else if back_button_resp.is_some_and(|r| r.clicked()) {
tracing::debug!("render nav action back");
Some(RenderNavAction::Back) Some(RenderNavAction::Back)
} else { } else {
None None
@@ -395,89 +397,95 @@ impl<'a> NavTitle<'a> {
.get_profile_by_pubkey(txn, pubkey) .get_profile_by_pubkey(txn, pubkey)
.as_ref() .as_ref()
.ok() .ok()
.and_then(move |p| Some(ProfilePic::from_profile(self.img_cache, p)?.size(pfp_size))) .and_then(move |p| {
Some(
ProfilePic::from_profile(self.img_cache, p)?
.size(pfp_size)
.sense(Sense::click()),
)
})
} }
fn timeline_pfp(&mut self, ui: &mut egui::Ui, id: &TimelineKind, pfp_size: f32) { fn timeline_pfp(&mut self, ui: &mut egui::Ui, id: &TimelineKind, pfp_size: f32) -> Response {
let txn = Transaction::new(self.ndb).unwrap(); let txn = Transaction::new(self.ndb).unwrap();
if let Some(mut pfp) = id if let Some(mut pfp) = id
.pubkey() .pubkey()
.and_then(|pk| self.pubkey_pfp(&txn, pk.bytes(), pfp_size)) .and_then(|pk| self.pubkey_pfp(&txn, pk.bytes(), pfp_size))
{ {
ui.add(&mut pfp); ui.add(&mut pfp)
} else { } else {
ui.add( ui.add(
&mut ProfilePic::new(self.img_cache, notedeck::profile::no_pfp_url()) &mut ProfilePic::new(self.img_cache, notedeck::profile::no_pfp_url())
.size(pfp_size), .size(pfp_size)
); .sense(Sense::click()),
)
} }
} }
fn title_pfp(&mut self, ui: &mut egui::Ui, top: &Route, pfp_size: f32) { fn title_pfp(&mut self, ui: &mut egui::Ui, top: &Route, pfp_size: f32) -> Option<Response> {
match top { match top {
Route::Timeline(kind) => match kind { Route::Timeline(kind) => match kind {
TimelineKind::Hashtag(_ht) => { TimelineKind::Hashtag(_ht) => Some(
ui.add( ui.add(
egui::Image::new(egui::include_image!( egui::Image::new(egui::include_image!(
"../../../../../assets/icons/hashtag_icon_4x.png" "../../../../../assets/icons/hashtag_icon_4x.png"
)) ))
.fit_to_exact_size(egui::vec2(pfp_size, pfp_size)), .fit_to_exact_size(egui::vec2(pfp_size, pfp_size)),
); ),
} ),
TimelineKind::Profile(pubkey) => { TimelineKind::Profile(pubkey) => Some(self.show_profile(ui, pubkey, pfp_size)),
self.show_profile(ui, pubkey, pfp_size);
}
TimelineKind::Thread(_) => { TimelineKind::Thread(_) => {
// no pfp for threads // no pfp for threads
None
} }
TimelineKind::Search(_sq) => { TimelineKind::Search(_sq) => {
// TODO: show author pfp if author field set? // TODO: show author pfp if author field set?
ui.add(ui::side_panel::search_button()); Some(ui.add(ui::side_panel::search_button()))
} }
TimelineKind::Universe TimelineKind::Universe
| TimelineKind::Algo(_) | TimelineKind::Algo(_)
| TimelineKind::Notifications(_) | TimelineKind::Notifications(_)
| TimelineKind::Generic(_) | TimelineKind::Generic(_)
| TimelineKind::List(_) => { | TimelineKind::List(_) => Some(self.timeline_pfp(ui, kind, pfp_size)),
self.timeline_pfp(ui, kind, pfp_size);
}
}, },
Route::Reply(_) => {} Route::Reply(_) => None,
Route::Quote(_) => {} Route::Quote(_) => None,
Route::Accounts(_as) => {} Route::Accounts(_as) => None,
Route::ComposeNote => {} Route::ComposeNote => None,
Route::AddColumn(_add_col_route) => {} Route::AddColumn(_add_col_route) => None,
Route::Support => {} Route::Support => None,
Route::Relays => {} Route::Relays => None,
Route::NewDeck => {} Route::NewDeck => None,
Route::EditDeck(_) => {} Route::EditDeck(_) => None,
Route::EditProfile(pubkey) => { Route::EditProfile(pubkey) => Some(self.show_profile(ui, pubkey, pfp_size)),
self.show_profile(ui, pubkey, pfp_size); Route::Search => Some(ui.add(ui::side_panel::search_button())),
} Route::Wallet(_) => None,
Route::Search => { Route::CustomizeZapAmount(_) => None,
ui.add(ui::side_panel::search_button());
}
Route::Wallet(_) => {}
Route::CustomizeZapAmount(_) => {}
} }
} }
fn show_profile(&mut self, ui: &mut egui::Ui, pubkey: &Pubkey, pfp_size: f32) { fn show_profile(
&mut self,
ui: &mut egui::Ui,
pubkey: &Pubkey,
pfp_size: f32,
) -> egui::Response {
let txn = Transaction::new(self.ndb).unwrap(); let txn = Transaction::new(self.ndb).unwrap();
if let Some(mut pfp) = self.pubkey_pfp(&txn, pubkey.bytes(), pfp_size) { if let Some(mut pfp) = self.pubkey_pfp(&txn, pubkey.bytes(), pfp_size) {
ui.add(&mut pfp); ui.add(&mut pfp)
} else { } else {
ui.add( ui.add(
&mut ProfilePic::new(self.img_cache, notedeck::profile::no_pfp_url()) &mut ProfilePic::new(self.img_cache, notedeck::profile::no_pfp_url())
.size(pfp_size), .size(pfp_size)
); .sense(Sense::click()),
}; )
}
} }
fn title_label_value(title: &str) -> egui::Label { fn title_label_value(title: &str) -> egui::Label {
@@ -489,27 +497,26 @@ impl<'a> NavTitle<'a> {
let column_title = top.title(); let column_title = top.title();
match &column_title { match &column_title {
ColumnTitle::Simple(title) => { ColumnTitle::Simple(title) => ui.add(Self::title_label_value(title)),
ui.add(Self::title_label_value(title));
}
ColumnTitle::NeedsDb(need_db) => { ColumnTitle::NeedsDb(need_db) => {
let txn = Transaction::new(self.ndb).unwrap(); let txn = Transaction::new(self.ndb).unwrap();
let title = need_db.title(&txn, self.ndb); let title = need_db.title(&txn, self.ndb);
ui.add(Self::title_label_value(title)); ui.add(Self::title_label_value(title))
} }
}; };
} }
fn title(&mut self, ui: &mut egui::Ui, top: &Route, navigating: bool) -> Option<TitleResponse> { fn title(&mut self, ui: &mut egui::Ui, top: &Route, navigating: bool) -> Option<TitleResponse> {
if !navigating { let title_r = if !navigating {
self.title_presentation(ui, top, 32.0); self.title_presentation(ui, top, 32.0)
} } else {
None
};
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if navigating { if navigating {
self.title_presentation(ui, top, 32.0); self.title_presentation(ui, top, 32.0)
None
} else { } else {
let move_col = self.move_button_section(ui); let move_col = self.move_button_section(ui);
let remove_col = self.delete_button_section(ui); let remove_col = self.delete_button_section(ui);
@@ -523,16 +530,37 @@ impl<'a> NavTitle<'a> {
} }
}) })
.inner .inner
.or(title_r)
} }
fn title_presentation(&mut self, ui: &mut egui::Ui, top: &Route, pfp_size: f32) { fn title_presentation(
self.title_pfp(ui, top, pfp_size); &mut self,
ui: &mut egui::Ui,
top: &Route,
pfp_size: f32,
) -> Option<TitleResponse> {
let pfp_r = self.title_pfp(ui, top, pfp_size);
if pfp_r.as_ref().is_some_and(|r| r.hovered()) {
notedeck_ui::show_pointer(ui);
}
self.title_label(ui, top); self.title_label(ui, top);
pfp_r.and_then(|r| {
if r.clicked() {
Some(TitleResponse::PfpClicked)
} else {
None
}
})
} }
} }
#[derive(Debug)]
enum TitleResponse { enum TitleResponse {
RemoveColumn, RemoveColumn,
PfpClicked,
MoveColumn(usize), MoveColumn(usize),
} }

View File

@@ -269,6 +269,11 @@ impl<'a, 'd> NoteView<'a, 'd> {
let pfp_resp = ui.put(rect, &mut pfp); let pfp_resp = ui.put(rect, &mut pfp);
action = action.or(pfp.action); action = action.or(pfp.action);
if resp.hovered() || resp.clicked() {
crate::show_pointer(ui);
}
pfp_resp.on_hover_ui_at_pointer(|ui| { pfp_resp.on_hover_ui_at_pointer(|ui| {
ui.set_max_width(300.0); ui.set_max_width(300.0);
ui.add(ProfilePreview::new( ui.add(ProfilePreview::new(
@@ -277,10 +282,6 @@ impl<'a, 'd> NoteView<'a, 'd> {
)); ));
}); });
if resp.hovered() || resp.clicked() {
crate::show_pointer(ui);
}
resp resp
} }

View File

@@ -9,13 +9,14 @@ pub struct ProfilePic<'cache, 'url> {
cache: &'cache mut Images, cache: &'cache mut Images,
url: &'url str, url: &'url str,
size: f32, size: f32,
sense: Sense,
border: Option<Stroke>, border: Option<Stroke>,
pub action: Option<MediaAction>, pub action: Option<MediaAction>,
} }
impl egui::Widget for &mut ProfilePic<'_, '_> { impl egui::Widget for &mut ProfilePic<'_, '_> {
fn ui(self, ui: &mut egui::Ui) -> egui::Response { fn ui(self, ui: &mut egui::Ui) -> egui::Response {
let inner = render_pfp(ui, self.cache, self.url, self.size, self.border); let inner = render_pfp(ui, self.cache, self.url, self.size, self.border, self.sense);
self.action = inner.inner; self.action = inner.inner;
@@ -26,8 +27,11 @@ impl egui::Widget for &mut ProfilePic<'_, '_> {
impl<'cache, 'url> ProfilePic<'cache, 'url> { impl<'cache, 'url> ProfilePic<'cache, 'url> {
pub fn new(cache: &'cache mut Images, url: &'url str) -> Self { pub fn new(cache: &'cache mut Images, url: &'url str) -> Self {
let size = Self::default_size() as f32; let size = Self::default_size() as f32;
let sense = Sense::hover();
ProfilePic { ProfilePic {
cache, cache,
sense,
url, url,
size, size,
border: None, border: None,
@@ -35,6 +39,11 @@ impl<'cache, 'url> ProfilePic<'cache, 'url> {
} }
} }
pub fn sense(mut self, sense: Sense) -> Self {
self.sense = sense;
self
}
pub fn border_stroke(ui: &egui::Ui) -> Stroke { pub fn border_stroke(ui: &egui::Ui) -> Stroke {
Stroke::new(4.0, ui.visuals().panel_fill) Stroke::new(4.0, ui.visuals().panel_fill)
} }
@@ -98,6 +107,7 @@ fn render_pfp(
url: &str, url: &str,
ui_size: f32, ui_size: f32,
border: Option<Stroke>, border: Option<Stroke>,
sense: Sense,
) -> InnerResponse<Option<MediaAction>> { ) -> InnerResponse<Option<MediaAction>> {
// We will want to downsample these so it's not blurry on hi res displays // We will want to downsample these so it's not blurry on hi res displays
let img_size = 128u32; let img_size = 128u32;
@@ -105,39 +115,39 @@ fn render_pfp(
let cache_type = supported_mime_hosted_at_url(&mut img_cache.urls, url) let cache_type = supported_mime_hosted_at_url(&mut img_cache.urls, url)
.unwrap_or(notedeck::MediaCacheType::Image); .unwrap_or(notedeck::MediaCacheType::Image);
egui::Frame::NONE.show(ui, |ui| { let cur_state = get_render_state(
let cur_state = get_render_state( ui.ctx(),
ui.ctx(), img_cache,
img_cache, cache_type,
cache_type, url,
url, ImageType::Profile(img_size),
ImageType::Profile(img_size), );
);
match cur_state.texture_state { match cur_state.texture_state {
notedeck::TextureState::Pending => { notedeck::TextureState::Pending => {
paint_circle(ui, ui_size, border); egui::InnerResponse::new(None, paint_circle(ui, ui_size, border, sense))
None }
} notedeck::TextureState::Error(e) => {
notedeck::TextureState::Error(e) => { let r = paint_circle(ui, ui_size, border, sense);
paint_circle(ui, ui_size, border); show_one_error_message(ui, &format!("Failed to fetch profile at url {url}: {e}"));
show_one_error_message(ui, &format!("Failed to fetch profile at url {url}: {e}")); egui::InnerResponse::new(
Some(MediaAction::FetchImage { Some(MediaAction::FetchImage {
url: url.to_owned(), url: url.to_owned(),
cache_type, cache_type,
no_pfp_promise: fetch_no_pfp_promise(ui.ctx(), img_cache.get_cache(cache_type)), no_pfp_promise: fetch_no_pfp_promise(ui.ctx(), img_cache.get_cache(cache_type)),
}) }),
} r,
notedeck::TextureState::Loaded(textured_image) => { )
let texture_handle = handle_repaint(
ui,
retrieve_latest_texture(url, cur_state.gifs, textured_image),
);
pfp_image(ui, texture_handle, ui_size, border);
None
}
} }
}) notedeck::TextureState::Loaded(textured_image) => {
let texture_handle = handle_repaint(
ui,
retrieve_latest_texture(url, cur_state.gifs, textured_image),
);
egui::InnerResponse::new(None, pfp_image(ui, texture_handle, ui_size, border, sense))
}
}
} }
#[profiling::function] #[profiling::function]
@@ -146,8 +156,9 @@ fn pfp_image(
img: &TextureHandle, img: &TextureHandle,
size: f32, size: f32,
border: Option<Stroke>, border: Option<Stroke>,
sense: Sense,
) -> egui::Response { ) -> egui::Response {
let (rect, response) = ui.allocate_at_least(vec2(size, size), Sense::hover()); let (rect, response) = ui.allocate_at_least(vec2(size, size), sense);
if let Some(stroke) = border { if let Some(stroke) = border {
draw_bg_border(ui, rect.center(), size, stroke); draw_bg_border(ui, rect.center(), size, stroke);
} }
@@ -156,8 +167,13 @@ fn pfp_image(
response response
} }
fn paint_circle(ui: &mut egui::Ui, size: f32, border: Option<Stroke>) -> egui::Response { fn paint_circle(
let (rect, response) = ui.allocate_at_least(vec2(size, size), Sense::hover()); ui: &mut egui::Ui,
size: f32,
border: Option<Stroke>,
sense: Sense,
) -> egui::Response {
let (rect, response) = ui.allocate_at_least(vec2(size, size), sense);
if let Some(stroke) = border { if let Some(stroke) = border {
draw_bg_border(ui, rect.center(), size, stroke); draw_bg_border(ui, rect.center(), size, stroke);