update sidebar to match new design

also adds interaction on hover & click

Signed-off-by: kernelkind <kernelkind@gmail.com>
This commit is contained in:
kernelkind 2024-09-20 17:10:35 -04:00
parent d76678dffc
commit 0ea3132ee2
2 changed files with 184 additions and 41 deletions

View File

@ -167,6 +167,14 @@ pub fn get_profile_url<'a>(profile: Option<&'a ProfileRecord<'a>>) -> &'a str {
} }
} }
pub fn get_profile_url_owned(profile: Option<ProfileRecord<'_>>) -> &str {
if let Some(url) = profile.and_then(|pr| pr.record().profile().and_then(|p| p.picture())) {
url
} else {
ProfilePic::no_pfp_url()
}
}
fn display_name_widget( fn display_name_widget(
display_name: DisplayName<'_>, display_name: DisplayName<'_>,
add_placeholder_space: bool, add_placeholder_space: bool,

View File

@ -1,16 +1,22 @@
use egui::{Button, InnerResponse, Layout, RichText, SidePanel, Vec2, Widget}; use egui::{vec2, Color32, InnerResponse, Layout, Margin, Separator, SidePanel, Stroke, Widget};
use tracing::info;
use crate::{ use crate::{
account_manager::AccountsRoute, account_manager::AccountsRoute,
colors,
column::Column, column::Column,
route::{Route, Router}, route::{Route, Router},
ui::profile_preview_controller,
Damus, Damus,
}; };
use super::{ProfilePic, View}; use super::{
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
profile::preview::{get_profile_url, get_profile_url_tmp},
ProfilePic, View,
};
pub static SIDE_PANEL_WIDTH: f32 = 64.0; pub static SIDE_PANEL_WIDTH: f32 = 64.0;
static ICON_WIDTH: f32 = 40.0;
pub struct DesktopSidePanel<'a> { pub struct DesktopSidePanel<'a> {
app: &'a mut Damus, app: &'a mut Damus,
@ -29,6 +35,7 @@ pub enum SidePanelAction {
Settings, Settings,
Columns, Columns,
ComposeNote, ComposeNote,
Search,
} }
pub struct SidePanelResponse { pub struct SidePanelResponse {
@ -50,24 +57,38 @@ impl<'a> DesktopSidePanel<'a> {
pub fn panel() -> SidePanel { pub fn panel() -> SidePanel {
egui::SidePanel::left("side_panel") egui::SidePanel::left("side_panel")
.resizable(false) .resizable(false)
.exact_width(40.0) .exact_width(SIDE_PANEL_WIDTH)
} }
pub fn show(&mut self, ui: &mut egui::Ui) -> SidePanelResponse { pub fn show(&mut self, ui: &mut egui::Ui) -> SidePanelResponse {
egui::Frame::none()
.inner_margin(Margin::same(8.0))
.show(ui, |ui| self.show_inner(ui))
.inner
}
fn show_inner(&mut self, ui: &mut egui::Ui) -> SidePanelResponse {
let dark_mode = ui.ctx().style().visuals.dark_mode; let dark_mode = ui.ctx().style().visuals.dark_mode;
let spacing_amt = 16.0;
let inner = ui let inner = ui
.vertical(|ui| { .vertical(|ui| {
let top_resp = ui let top_resp = ui
.with_layout(Layout::top_down(egui::Align::Center), |ui| { .with_layout(Layout::top_down(egui::Align::Center), |ui| {
let compose_resp = ui.add(compose_note_button()); let compose_resp = ui.add(compose_note_button());
let search_resp = ui.add(search_button());
let column_resp = ui.add(add_column_button(dark_mode));
ui.add(Separator::default().horizontal().spacing(8.0).shrink(4.0));
if compose_resp.clicked() { if compose_resp.clicked() {
Some(InnerResponse::new( Some(InnerResponse::new(
SidePanelAction::ComposeNote, SidePanelAction::ComposeNote,
compose_resp, compose_resp,
)) ))
} else if search_resp.clicked() {
Some(InnerResponse::new(SidePanelAction::Search, search_resp))
} else if column_resp.clicked() {
Some(InnerResponse::new(SidePanelAction::Columns, column_resp))
} else { } else {
None None
} }
@ -76,10 +97,8 @@ impl<'a> DesktopSidePanel<'a> {
let (pfp_resp, bottom_resp) = ui let (pfp_resp, bottom_resp) = ui
.with_layout(Layout::bottom_up(egui::Align::Center), |ui| { .with_layout(Layout::bottom_up(egui::Align::Center), |ui| {
ui.spacing_mut().item_spacing.y = spacing_amt;
let pfp_resp = self.pfp_button(ui); let pfp_resp = self.pfp_button(ui);
let settings_resp = ui.add(settings_button(dark_mode)); let settings_resp = ui.add(settings_button(dark_mode));
let column_resp = ui.add(add_column_button(dark_mode));
let optional_inner = if pfp_resp.clicked() { let optional_inner = if pfp_resp.clicked() {
Some(egui::InnerResponse::new( Some(egui::InnerResponse::new(
@ -91,11 +110,6 @@ impl<'a> DesktopSidePanel<'a> {
SidePanelAction::Settings, SidePanelAction::Settings,
settings_resp, settings_resp,
)) ))
} else if column_resp.clicked() || column_resp.hovered() {
Some(egui::InnerResponse::new(
SidePanelAction::Columns,
column_resp,
))
} else { } else {
None None
}; };
@ -118,13 +132,33 @@ impl<'a> DesktopSidePanel<'a> {
} }
fn pfp_button(&mut self, ui: &mut egui::Ui) -> egui::Response { fn pfp_button(&mut self, ui: &mut egui::Ui) -> egui::Response {
if let Some(resp) = let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
profile_preview_controller::show_with_selected_pfp(self.app, ui, show_pfp()) let helper = AnimationHelper::new(ui, "pfp-button", vec2(max_size, max_size));
{
resp let min_pfp_size = ICON_WIDTH;
let cur_pfp_size = helper.scale_1d_pos(min_pfp_size);
let selected_account = self.app.accounts().get_selected_account();
let txn = nostrdb::Transaction::new(&self.app.ndb).expect("should be able to create txn");
let profile_url = if let Some(selected_account) = selected_account {
if let Ok(profile) = self
.app
.ndb()
.get_profile_by_pubkey(&txn, selected_account.pubkey.bytes())
{
get_profile_url_tmp(Some(profile))
} else {
get_profile_url_tmp(None)
}
} else { } else {
add_button_to_ui(ui, no_account_pfp()) get_profile_url(None)
} };
let widget = ProfilePic::new(self.app.img_cache_mut(), profile_url).size(cur_pfp_size);
ui.put(helper.get_animation_rect(), widget);
helper.take_animation_response()
} }
pub fn perform_action(router: &mut Router<Route>, action: SidePanelAction) { pub fn perform_action(router: &mut Router<Route>, action: SidePanelAction) {
@ -150,7 +184,10 @@ impl<'a> DesktopSidePanel<'a> {
router.route_to(Route::relays()); router.route_to(Route::relays());
} }
} }
SidePanelAction::Columns => (), // TODO SidePanelAction::Columns => {
// TODO
info!("Clicked columns button");
}
SidePanelAction::ComposeNote => { SidePanelAction::ComposeNote => {
if router.routes().iter().any(|&r| r == Route::ComposeNote) { if router.routes().iter().any(|&r| r == Route::ComposeNote) {
router.go_back(); router.go_back();
@ -158,43 +195,141 @@ impl<'a> DesktopSidePanel<'a> {
router.route_to(Route::ComposeNote); router.route_to(Route::ComposeNote);
} }
} }
SidePanelAction::Search => {
// TODO
info!("Clicked search button");
}
} }
} }
} }
fn show_pfp() -> fn(ui: &mut egui::Ui, pfp: ProfilePic) -> egui::Response { fn settings_button(dark_mode: bool) -> impl Widget {
|ui, pfp| { let _ = dark_mode;
let response = pfp.ui(ui); |ui: &mut egui::Ui| {
ui.allocate_rect(response.rect, egui::Sense::click()) let img_size = 24.0;
let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
let img_data = egui::include_image!("../../assets/icons/settings_dark_4x.png");
let img = egui::Image::new(img_data).max_width(img_size);
let helper = AnimationHelper::new(ui, "settings-button", vec2(max_size, max_size));
let cur_img_size = helper.scale_1d_pos(img_size);
img.paint_at(
ui,
helper
.get_animation_rect()
.shrink((max_size - cur_img_size) / 2.0),
);
helper.take_animation_response()
} }
} }
fn settings_button(dark_mode: bool) -> egui::Button<'static> { fn add_column_button(dark_mode: bool) -> impl Widget {
let _ = dark_mode; let _ = dark_mode;
let img_data = egui::include_image!("../../assets/icons/settings_dark_4x.png"); move |ui: &mut egui::Ui| {
let img_size = 24.0;
let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
egui::Button::image(egui::Image::new(img_data).max_width(32.0)).frame(false) let img_data = egui::include_image!("../../assets/icons/add_column_dark_4x.png");
let img = egui::Image::new(img_data).max_width(img_size);
let helper = AnimationHelper::new(ui, "add-column-button", vec2(max_size, max_size));
let cur_img_size = helper.scale_1d_pos(img_size);
img.paint_at(
ui,
helper
.get_animation_rect()
.shrink((max_size - cur_img_size) / 2.0),
);
helper.take_animation_response()
}
} }
fn add_button_to_ui(ui: &mut egui::Ui, button: Button) -> egui::Response { fn compose_note_button() -> impl Widget {
ui.add_sized(Vec2::new(32.0, 32.0), button) |ui: &mut egui::Ui| -> egui::Response {
let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
let min_outer_circle_diameter = 40.0;
let min_plus_sign_size = 14.0; // length of the plus sign
let min_line_width = 2.25; // width of the plus sign
let helper = AnimationHelper::new(ui, "note-compose-button", vec2(max_size, max_size));
let painter = ui.painter_at(helper.get_animation_rect());
let use_background_radius = helper.scale_radius(min_outer_circle_diameter);
let use_line_width = helper.scale_1d_pos(min_line_width);
let use_edge_circle_radius = helper.scale_radius(min_line_width);
painter.circle_filled(helper.center(), use_background_radius, colors::PINK);
let min_half_plus_sign_size = min_plus_sign_size / 2.0;
let north_edge = helper.scale_from_center(0.0, min_half_plus_sign_size);
let south_edge = helper.scale_from_center(0.0, -min_half_plus_sign_size);
let west_edge = helper.scale_from_center(-min_half_plus_sign_size, 0.0);
let east_edge = helper.scale_from_center(min_half_plus_sign_size, 0.0);
painter.line_segment(
[north_edge, south_edge],
Stroke::new(use_line_width, Color32::WHITE),
);
painter.line_segment(
[west_edge, east_edge],
Stroke::new(use_line_width, Color32::WHITE),
);
painter.circle_filled(north_edge, use_edge_circle_radius, Color32::WHITE);
painter.circle_filled(south_edge, use_edge_circle_radius, Color32::WHITE);
painter.circle_filled(west_edge, use_edge_circle_radius, Color32::WHITE);
painter.circle_filled(east_edge, use_edge_circle_radius, Color32::WHITE);
helper.take_animation_response()
}
} }
fn no_account_pfp() -> Button<'static> { fn search_button() -> impl Widget {
Button::new("A") |ui: &mut egui::Ui| -> egui::Response {
.rounding(20.0) let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
.min_size(Vec2::new(38.0, 38.0)) let min_line_width_circle = 1.5; // width of the magnifying glass
} let min_line_width_handle = 1.5;
let helper = AnimationHelper::new(ui, "search-button", vec2(max_size, max_size));
fn add_column_button(dark_mode: bool) -> egui::Button<'static> { let painter = ui.painter_at(helper.get_animation_rect());
let _ = dark_mode;
let img_data = egui::include_image!("../../assets/icons/add_column_dark_4x.png");
egui::Button::image(egui::Image::new(img_data).max_width(32.0)).frame(false) let cur_line_width_circle = helper.scale_1d_pos(min_line_width_circle);
} let cur_line_width_handle = helper.scale_1d_pos(min_line_width_handle);
let min_outer_circle_radius = helper.scale_radius(15.0);
let cur_outer_circle_radius = helper.scale_1d_pos(min_outer_circle_radius);
let min_handle_length = 7.0;
let cur_handle_length = helper.scale_1d_pos(min_handle_length);
fn compose_note_button() -> Button<'static> { let circle_center = helper.scale_from_center(-2.0, -2.0);
Button::new(RichText::new("+").size(32.0)).frame(false)
let handle_vec = vec2(
std::f32::consts::FRAC_1_SQRT_2,
std::f32::consts::FRAC_1_SQRT_2,
);
let handle_pos_1 = circle_center + (handle_vec * (cur_outer_circle_radius - 3.0));
let handle_pos_2 =
circle_center + (handle_vec * (cur_outer_circle_radius + cur_handle_length));
let circle_stroke = Stroke::new(cur_line_width_circle, colors::MID_GRAY);
let handle_stroke = Stroke::new(cur_line_width_handle, colors::MID_GRAY);
painter.line_segment([handle_pos_1, handle_pos_2], handle_stroke);
painter.circle(
circle_center,
min_outer_circle_radius,
ui.style().visuals.widgets.inactive.weak_bg_fill,
circle_stroke,
);
helper.take_animation_response()
}
} }
mod preview { mod preview {
@ -225,7 +360,7 @@ mod preview {
impl View for DesktopSidePanelPreview { impl View for DesktopSidePanelPreview {
fn ui(&mut self, ui: &mut egui::Ui) { fn ui(&mut self, ui: &mut egui::Ui) {
StripBuilder::new(ui) StripBuilder::new(ui)
.size(Size::exact(40.0)) .size(Size::exact(SIDE_PANEL_WIDTH))
.sizes(Size::remainder(), 0) .sizes(Size::remainder(), 0)
.clip(true) .clip(true)
.horizontal(|mut strip| { .horizontal(|mut strip| {