454 lines
16 KiB
Rust
454 lines
16 KiB
Rust
use egui::{vec2, Color32, InnerResponse, Layout, Margin, Separator, Stroke, Widget};
|
|
use tracing::info;
|
|
|
|
use crate::{
|
|
account_manager::AccountsRoute,
|
|
colors,
|
|
column::{Column, Columns},
|
|
imgcache::ImageCache,
|
|
route::Route,
|
|
support::Support,
|
|
user_account::UserAccount,
|
|
Damus,
|
|
};
|
|
|
|
use super::{
|
|
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
|
|
profile::preview::get_account_url,
|
|
ProfilePic, View,
|
|
};
|
|
|
|
pub static SIDE_PANEL_WIDTH: f32 = 64.0;
|
|
static ICON_WIDTH: f32 = 40.0;
|
|
|
|
pub struct DesktopSidePanel<'a> {
|
|
ndb: &'a nostrdb::Ndb,
|
|
img_cache: &'a mut ImageCache,
|
|
selected_account: Option<&'a UserAccount>,
|
|
}
|
|
|
|
impl<'a> View for DesktopSidePanel<'a> {
|
|
fn ui(&mut self, ui: &mut egui::Ui) {
|
|
self.show(ui);
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
|
|
pub enum SidePanelAction {
|
|
Panel,
|
|
Account,
|
|
Settings,
|
|
Columns,
|
|
ComposeNote,
|
|
Search,
|
|
ExpandSidePanel,
|
|
Support,
|
|
}
|
|
|
|
pub struct SidePanelResponse {
|
|
pub response: egui::Response,
|
|
pub action: SidePanelAction,
|
|
}
|
|
|
|
impl SidePanelResponse {
|
|
fn new(action: SidePanelAction, response: egui::Response) -> Self {
|
|
SidePanelResponse { action, response }
|
|
}
|
|
}
|
|
|
|
impl<'a> DesktopSidePanel<'a> {
|
|
pub fn new(
|
|
ndb: &'a nostrdb::Ndb,
|
|
img_cache: &'a mut ImageCache,
|
|
selected_account: Option<&'a UserAccount>,
|
|
) -> Self {
|
|
Self {
|
|
ndb,
|
|
img_cache,
|
|
selected_account,
|
|
}
|
|
}
|
|
|
|
pub fn 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 inner = ui
|
|
.vertical(|ui| {
|
|
let top_resp = ui
|
|
.with_layout(Layout::top_down(egui::Align::Center), |ui| {
|
|
let expand_resp = ui.add(expand_side_panel_button());
|
|
ui.add_space(28.0);
|
|
let compose_resp = ui.add(compose_note_button());
|
|
let search_resp = ui.add(search_button());
|
|
let column_resp = ui.add(add_column_button(dark_mode));
|
|
|
|
ui.add(Separator::default().horizontal().spacing(8.0).shrink(4.0));
|
|
|
|
if expand_resp.clicked() {
|
|
Some(InnerResponse::new(
|
|
SidePanelAction::ExpandSidePanel,
|
|
expand_resp,
|
|
))
|
|
} else if compose_resp.clicked() {
|
|
Some(InnerResponse::new(
|
|
SidePanelAction::ComposeNote,
|
|
compose_resp,
|
|
))
|
|
} else if search_resp.clicked() {
|
|
Some(InnerResponse::new(SidePanelAction::Search, search_resp))
|
|
} else if column_resp.clicked() {
|
|
Some(InnerResponse::new(SidePanelAction::Columns, column_resp))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.inner;
|
|
|
|
let (pfp_resp, bottom_resp) = ui
|
|
.with_layout(Layout::bottom_up(egui::Align::Center), |ui| {
|
|
let pfp_resp = self.pfp_button(ui);
|
|
let settings_resp = ui.add(settings_button(dark_mode));
|
|
|
|
let support_resp = ui.add(support_button());
|
|
|
|
let optional_inner = if pfp_resp.clicked() {
|
|
Some(egui::InnerResponse::new(
|
|
SidePanelAction::Account,
|
|
pfp_resp.clone(),
|
|
))
|
|
} else if settings_resp.clicked() || settings_resp.hovered() {
|
|
Some(egui::InnerResponse::new(
|
|
SidePanelAction::Settings,
|
|
settings_resp,
|
|
))
|
|
} else if support_resp.clicked() {
|
|
Some(egui::InnerResponse::new(
|
|
SidePanelAction::Support,
|
|
support_resp,
|
|
))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
(pfp_resp, optional_inner)
|
|
})
|
|
.inner;
|
|
|
|
if let Some(bottom_inner) = bottom_resp {
|
|
bottom_inner
|
|
} else if let Some(top_inner) = top_resp {
|
|
top_inner
|
|
} else {
|
|
egui::InnerResponse::new(SidePanelAction::Panel, pfp_resp)
|
|
}
|
|
})
|
|
.inner;
|
|
|
|
SidePanelResponse::new(inner.inner, inner.response)
|
|
}
|
|
|
|
fn pfp_button(&mut self, ui: &mut egui::Ui) -> egui::Response {
|
|
let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
|
|
let helper = AnimationHelper::new(ui, "pfp-button", vec2(max_size, max_size));
|
|
|
|
let min_pfp_size = ICON_WIDTH;
|
|
let cur_pfp_size = helper.scale_1d_pos(min_pfp_size);
|
|
|
|
let txn = nostrdb::Transaction::new(self.ndb).expect("should be able to create txn");
|
|
let profile_url = get_account_url(&txn, self.ndb, self.selected_account);
|
|
|
|
let widget = ProfilePic::new(self.img_cache, profile_url).size(cur_pfp_size);
|
|
|
|
ui.put(helper.get_animation_rect(), widget);
|
|
|
|
helper.take_animation_response()
|
|
}
|
|
|
|
pub fn perform_action(columns: &mut Columns, support: &mut Support, action: SidePanelAction) {
|
|
let router = columns.get_first_router();
|
|
match action {
|
|
SidePanelAction::Panel => {} // TODO
|
|
SidePanelAction::Account => {
|
|
if router
|
|
.routes()
|
|
.iter()
|
|
.any(|&r| r == Route::Accounts(AccountsRoute::Accounts))
|
|
{
|
|
// return if we are already routing to accounts
|
|
router.go_back();
|
|
} else {
|
|
router.route_to(Route::accounts());
|
|
}
|
|
}
|
|
SidePanelAction::Settings => {
|
|
if router.routes().iter().any(|&r| r == Route::Relays) {
|
|
// return if we are already routing to accounts
|
|
router.go_back();
|
|
} else {
|
|
router.route_to(Route::relays());
|
|
}
|
|
}
|
|
SidePanelAction::Columns => {
|
|
if router
|
|
.routes()
|
|
.iter()
|
|
.any(|&r| matches!(r, Route::AddColumn(_)))
|
|
{
|
|
router.go_back();
|
|
} else {
|
|
columns.new_column_picker();
|
|
}
|
|
}
|
|
SidePanelAction::ComposeNote => {
|
|
if router.routes().iter().any(|&r| r == Route::ComposeNote) {
|
|
router.go_back();
|
|
} else {
|
|
router.route_to(Route::ComposeNote);
|
|
}
|
|
}
|
|
SidePanelAction::Search => {
|
|
// TODO
|
|
info!("Clicked search button");
|
|
}
|
|
SidePanelAction::ExpandSidePanel => {
|
|
// TODO
|
|
info!("Clicked expand side panel button");
|
|
}
|
|
SidePanelAction::Support => {
|
|
if router.routes().iter().any(|&r| r == Route::Support) {
|
|
router.go_back();
|
|
} else {
|
|
support.refresh();
|
|
router.route_to(Route::Support);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn settings_button(dark_mode: bool) -> impl Widget {
|
|
let _ = dark_mode;
|
|
|ui: &mut egui::Ui| {
|
|
let img_size = 24.0;
|
|
let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
|
|
let img_data = egui::include_image!("../../assets/icons/settings_dark_4x.png");
|
|
let img = egui::Image::new(img_data).max_width(img_size);
|
|
|
|
let helper = AnimationHelper::new(ui, "settings-button", vec2(max_size, max_size));
|
|
|
|
let cur_img_size = helper.scale_1d_pos(img_size);
|
|
img.paint_at(
|
|
ui,
|
|
helper
|
|
.get_animation_rect()
|
|
.shrink((max_size - cur_img_size) / 2.0),
|
|
);
|
|
|
|
helper.take_animation_response()
|
|
}
|
|
}
|
|
|
|
fn add_column_button(dark_mode: bool) -> impl Widget {
|
|
let _ = dark_mode;
|
|
move |ui: &mut egui::Ui| {
|
|
let img_size = 24.0;
|
|
let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
|
|
|
|
let img_data = egui::include_image!("../../assets/icons/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 compose_note_button() -> impl Widget {
|
|
|ui: &mut egui::Ui| -> egui::Response {
|
|
let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
|
|
|
|
let min_outer_circle_diameter = 40.0;
|
|
let min_plus_sign_size = 14.0; // length of the plus sign
|
|
let min_line_width = 2.25; // width of the plus sign
|
|
|
|
let helper = AnimationHelper::new(ui, "note-compose-button", vec2(max_size, max_size));
|
|
|
|
let painter = ui.painter_at(helper.get_animation_rect());
|
|
|
|
let use_background_radius = helper.scale_radius(min_outer_circle_diameter);
|
|
let use_line_width = helper.scale_1d_pos(min_line_width);
|
|
let use_edge_circle_radius = helper.scale_radius(min_line_width);
|
|
|
|
painter.circle_filled(helper.center(), use_background_radius, colors::PINK);
|
|
|
|
let min_half_plus_sign_size = min_plus_sign_size / 2.0;
|
|
let north_edge = helper.scale_from_center(0.0, min_half_plus_sign_size);
|
|
let south_edge = helper.scale_from_center(0.0, -min_half_plus_sign_size);
|
|
let west_edge = helper.scale_from_center(-min_half_plus_sign_size, 0.0);
|
|
let east_edge = helper.scale_from_center(min_half_plus_sign_size, 0.0);
|
|
|
|
painter.line_segment(
|
|
[north_edge, south_edge],
|
|
Stroke::new(use_line_width, Color32::WHITE),
|
|
);
|
|
painter.line_segment(
|
|
[west_edge, east_edge],
|
|
Stroke::new(use_line_width, Color32::WHITE),
|
|
);
|
|
painter.circle_filled(north_edge, use_edge_circle_radius, Color32::WHITE);
|
|
painter.circle_filled(south_edge, use_edge_circle_radius, Color32::WHITE);
|
|
painter.circle_filled(west_edge, use_edge_circle_radius, Color32::WHITE);
|
|
painter.circle_filled(east_edge, use_edge_circle_radius, Color32::WHITE);
|
|
|
|
helper.take_animation_response()
|
|
}
|
|
}
|
|
|
|
fn search_button() -> impl Widget {
|
|
|ui: &mut egui::Ui| -> egui::Response {
|
|
let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
|
|
let min_line_width_circle = 1.5; // width of the magnifying glass
|
|
let min_line_width_handle = 1.5;
|
|
let helper = AnimationHelper::new(ui, "search-button", vec2(max_size, max_size));
|
|
|
|
let painter = ui.painter_at(helper.get_animation_rect());
|
|
|
|
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);
|
|
|
|
let circle_center = helper.scale_from_center(-2.0, -2.0);
|
|
|
|
let handle_vec = vec2(
|
|
std::f32::consts::FRAC_1_SQRT_2,
|
|
std::f32::consts::FRAC_1_SQRT_2,
|
|
);
|
|
|
|
let handle_pos_1 = circle_center + (handle_vec * (cur_outer_circle_radius - 3.0));
|
|
let handle_pos_2 =
|
|
circle_center + (handle_vec * (cur_outer_circle_radius + cur_handle_length));
|
|
|
|
let circle_stroke = Stroke::new(cur_line_width_circle, colors::MID_GRAY);
|
|
let handle_stroke = Stroke::new(cur_line_width_handle, colors::MID_GRAY);
|
|
|
|
painter.line_segment([handle_pos_1, handle_pos_2], handle_stroke);
|
|
painter.circle(
|
|
circle_center,
|
|
min_outer_circle_radius,
|
|
ui.style().visuals.widgets.inactive.weak_bg_fill,
|
|
circle_stroke,
|
|
);
|
|
|
|
helper.take_animation_response()
|
|
}
|
|
}
|
|
|
|
// TODO: convert to responsive button when expanded side panel impl is finished
|
|
fn expand_side_panel_button() -> impl Widget {
|
|
|ui: &mut egui::Ui| -> egui::Response {
|
|
let img_size = 40.0;
|
|
let img_data = egui::include_image!("../../assets/damus_rounded_80.png");
|
|
let img = egui::Image::new(img_data).max_width(img_size);
|
|
|
|
ui.add(img)
|
|
}
|
|
}
|
|
|
|
fn support_button() -> impl Widget {
|
|
|ui: &mut egui::Ui| -> egui::Response {
|
|
let img_size = 16.0;
|
|
|
|
let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
|
|
let img_data = egui::include_image!("../../assets/icons/help_icon_dark_4x.png");
|
|
let img = egui::Image::new(img_data).max_width(img_size);
|
|
|
|
let helper = AnimationHelper::new(ui, "help-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()
|
|
}
|
|
}
|
|
|
|
mod preview {
|
|
|
|
use egui_extras::{Size, StripBuilder};
|
|
|
|
use crate::{
|
|
test_data,
|
|
ui::{Preview, PreviewConfig},
|
|
};
|
|
|
|
use super::*;
|
|
|
|
pub struct DesktopSidePanelPreview {
|
|
app: Damus,
|
|
}
|
|
|
|
impl DesktopSidePanelPreview {
|
|
fn new() -> Self {
|
|
let mut app = test_data::test_app();
|
|
app.columns.add_column(Column::new(vec![Route::accounts()]));
|
|
DesktopSidePanelPreview { app }
|
|
}
|
|
}
|
|
|
|
impl View for DesktopSidePanelPreview {
|
|
fn ui(&mut self, ui: &mut egui::Ui) {
|
|
StripBuilder::new(ui)
|
|
.size(Size::exact(SIDE_PANEL_WIDTH))
|
|
.sizes(Size::remainder(), 0)
|
|
.clip(true)
|
|
.horizontal(|mut strip| {
|
|
strip.cell(|ui| {
|
|
let mut panel = DesktopSidePanel::new(
|
|
&self.app.ndb,
|
|
&mut self.app.img_cache,
|
|
self.app.accounts.get_selected_account(),
|
|
);
|
|
let response = panel.show(ui);
|
|
|
|
DesktopSidePanel::perform_action(
|
|
&mut self.app.columns,
|
|
&mut self.app.support,
|
|
response.action,
|
|
);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
impl<'a> Preview for DesktopSidePanel<'a> {
|
|
type Prev = DesktopSidePanelPreview;
|
|
|
|
fn preview(_cfg: PreviewConfig) -> Self::Prev {
|
|
DesktopSidePanelPreview::new()
|
|
}
|
|
}
|
|
}
|