commit 7fe3d5e99f25d45bcd8c359d2f50ddd646de9614
parent 7e4bddcacbfa468b7b55d195d25a34ae0e343c4c
Author: William Casarin <jb55@jb55.com>
Date: Thu, 26 Sep 2024 13:17:59 -0700
Merge side panel update #327
A few merge conflicts resolved, and changes the image to svg
kernelkind (7):
initial compose note view
change side panel width to 64.0
Add AnimationHelper
update sidebar to match new design
remove app from sidebar
remove profile_preview_controller
add logo to side panel
Closes: https://github.com/damus-io/notedeck/pull/327
Diffstat:
13 files changed, 739 insertions(+), 240 deletions(-)
diff --git a/assets/damus_rounded.svg b/assets/damus_rounded.svg
@@ -0,0 +1,334 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="256mm"
+ height="256mm"
+ viewBox="0 0 256 256"
+ version="1.1"
+ id="svg5"
+ inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
+ sodipodi:docname="damus_rounded.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview
+ id="namedview7"
+ pagecolor="#ffffff"
+ bordercolor="#000000"
+ borderopacity="0.25"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ inkscape:blackoutopacity="0.0"
+ inkscape:document-units="mm"
+ showgrid="false"
+ inkscape:zoom="0.5946522"
+ inkscape:cx="405.27892"
+ inkscape:cy="543.17465"
+ inkscape:window-width="1920"
+ inkscape:window-height="1080"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="layer1"
+ inkscape:showpageshadow="2"
+ inkscape:deskcolor="#d1d1d1" />
+ <defs
+ id="defs2">
+ <inkscape:path-effect
+ effect="fillet_chamfer"
+ id="path-effect9"
+ is_visible="true"
+ lpeversion="1"
+ nodesatellites_param="F,0,0,1,0,80,0,1 @ F,0,1,1,0,80,0,1 @ F,0,0,1,0,80,0,1 @ F,0,0,1,0,80,0,1"
+ radius="0"
+ unit="px"
+ method="auto"
+ mode="F"
+ chamfer_steps="1"
+ flexible="false"
+ use_knot_distance="true"
+ apply_no_radius="true"
+ apply_with_radius="true"
+ only_selected="false"
+ hide_knots="false" />
+ <inkscape:path-effect
+ effect="fillet_chamfer"
+ id="path-effect8"
+ is_visible="true"
+ lpeversion="1"
+ nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
+ radius="0"
+ unit="px"
+ method="auto"
+ mode="F"
+ chamfer_steps="1"
+ flexible="false"
+ use_knot_distance="true"
+ apply_no_radius="true"
+ apply_with_radius="true"
+ only_selected="false"
+ hide_knots="false" />
+ <inkscape:path-effect
+ effect="fillet_chamfer"
+ id="path-effect7"
+ is_visible="true"
+ lpeversion="1"
+ nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
+ radius="0"
+ unit="px"
+ method="auto"
+ mode="F"
+ chamfer_steps="1"
+ flexible="false"
+ use_knot_distance="true"
+ apply_no_radius="true"
+ apply_with_radius="true"
+ only_selected="false"
+ hide_knots="false" />
+ <inkscape:path-effect
+ effect="fillet_chamfer"
+ id="path-effect6"
+ is_visible="true"
+ lpeversion="1"
+ nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
+ radius="0"
+ unit="px"
+ method="auto"
+ mode="F"
+ chamfer_steps="1"
+ flexible="false"
+ use_knot_distance="true"
+ apply_no_radius="true"
+ apply_with_radius="true"
+ only_selected="false"
+ hide_knots="false" />
+ <inkscape:path-effect
+ effect="fillet_chamfer"
+ id="path-effect5"
+ is_visible="true"
+ lpeversion="1"
+ nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
+ radius="0"
+ unit="px"
+ method="auto"
+ mode="F"
+ chamfer_steps="1"
+ flexible="false"
+ use_knot_distance="true"
+ apply_no_radius="true"
+ apply_with_radius="true"
+ only_selected="false"
+ hide_knots="false" />
+ <inkscape:path-effect
+ effect="fillet_chamfer"
+ id="path-effect4"
+ is_visible="true"
+ lpeversion="1"
+ nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
+ radius="0"
+ unit="px"
+ method="auto"
+ mode="F"
+ chamfer_steps="1"
+ flexible="false"
+ use_knot_distance="true"
+ apply_no_radius="true"
+ apply_with_radius="true"
+ only_selected="false"
+ hide_knots="false" />
+ <inkscape:path-effect
+ effect="fillet_chamfer"
+ id="path-effect3"
+ is_visible="true"
+ lpeversion="1"
+ nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
+ radius="0"
+ unit="px"
+ method="auto"
+ mode="F"
+ chamfer_steps="1"
+ flexible="false"
+ use_knot_distance="true"
+ apply_no_radius="true"
+ apply_with_radius="true"
+ only_selected="false"
+ hide_knots="false" />
+ <inkscape:path-effect
+ effect="fillet_chamfer"
+ id="path-effect2"
+ is_visible="true"
+ lpeversion="1"
+ nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
+ radius="0"
+ unit="px"
+ method="auto"
+ mode="F"
+ chamfer_steps="1"
+ flexible="false"
+ use_knot_distance="true"
+ apply_no_radius="true"
+ apply_with_radius="true"
+ only_selected="false"
+ hide_knots="false" />
+ <inkscape:path-effect
+ effect="fillet_chamfer"
+ id="path-effect1"
+ is_visible="true"
+ lpeversion="1"
+ nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
+ radius="0"
+ unit="px"
+ method="auto"
+ mode="F"
+ chamfer_steps="1"
+ flexible="false"
+ use_knot_distance="true"
+ apply_no_radius="true"
+ apply_with_radius="true"
+ only_selected="false"
+ hide_knots="false" />
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient39361">
+ <stop
+ style="stop-color:#0de8ff;stop-opacity:0.78082192;"
+ offset="0"
+ id="stop39357" />
+ <stop
+ style="stop-color:#d600fc;stop-opacity:0.95433789;"
+ offset="1"
+ id="stop39359" />
+ </linearGradient>
+ <inkscape:path-effect
+ effect="bspline"
+ id="path-effect255"
+ is_visible="true"
+ lpeversion="1"
+ weight="33.333333"
+ steps="2"
+ helper_size="0"
+ apply_no_weight="true"
+ apply_with_weight="true"
+ only_selected="false" />
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient2119">
+ <stop
+ style="stop-color:#1c55ff;stop-opacity:1;"
+ offset="0"
+ id="stop2115" />
+ <stop
+ style="stop-color:#7f35ab;stop-opacity:1;"
+ offset="0.5"
+ id="stop2123" />
+ <stop
+ style="stop-color:#ff0bd6;stop-opacity:1;"
+ offset="1"
+ id="stop2117" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2119"
+ id="linearGradient2121"
+ x1="10.067794"
+ y1="248.81357"
+ x2="246.56145"
+ y2="7.1864405"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient39361"
+ id="linearGradient39367"
+ x1="62.104473"
+ y1="128.78963"
+ x2="208.25758"
+ y2="128.78963"
+ gradientUnits="userSpaceOnUse" />
+ </defs>
+ <g
+ inkscape:label="Background"
+ inkscape:groupmode="layer"
+ id="layer1"
+ sodipodi:insensitive="true">
+ <path
+ id="rect61"
+ style="fill:url(#linearGradient2121);stroke-width:0.264583;opacity:1"
+ inkscape:label="Gradient"
+ d="m 80,-1.0775033e-7 h 96 A 80,80 45 0 1 256,80 v 96 a 80,80 135 0 1 -80,80 H 80 A 80,80 45 0 1 -5.3875166e-8,176 V 80 A 80,80 135 0 1 80,-1.0775033e-7 Z"
+ inkscape:path-effect="#path-effect9"
+ inkscape:original-d="M -5.3875166e-8,-1.0775033e-7 H 256 V 256 H -5.3875166e-8 Z" />
+ </g>
+ <g
+ id="g407"
+ inkscape:label="Logo">
+ <g
+ id="layer2"
+ inkscape:label="LogoStroke"
+ style="display:inline">
+ <path
+ style="fill:url(#linearGradient39367);fill-opacity:1;stroke:#ffffff;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 101.1429,213.87373 C 67.104473,239.1681 67.104473,42.67112 67.104473,42.67112 135.18122,57.58146 203.25844,72.491904 203.25758,105.24181 c -8.6e-4,32.74991 -68.07625,83.33755 -102.11468,108.63192 z"
+ id="path253" />
+ </g>
+ <g
+ inkscape:groupmode="layer"
+ id="layer3"
+ inkscape:label="Poly">
+ <path
+ style="fill:#ffffff;fill-opacity:0.325424;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 67.32839,76.766948 112.00424,99.41949 100.04873,52.226693 Z"
+ id="path4648" />
+ <path
+ style="fill:#ffffff;fill-opacity:0.274576;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 111.45696,98.998695 107.00758,142.60261 70.077729,105.67276 Z"
+ id="path9299" />
+ <path
+ style="fill:#ffffff;fill-opacity:0.379661;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 111.01202,99.221164 29.14343,-37.15232 25.80641,39.377006 z"
+ id="path9301" />
+ <path
+ style="fill:#ffffff;fill-opacity:0.447458;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 111.45696,99.443631 57.17452,55.172309 -2.89209,-53.17009 z"
+ id="path9368" />
+ <path
+ style="fill:#ffffff;fill-opacity:0.20678;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 106.78511,142.38015 62.06884,12.68073 -57.17452,-55.617249 z"
+ id="path9370" />
+ <path
+ style="fill:#ffffff;fill-opacity:0.244068;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 106.78511,142.38015 -28.47603,32.9254 62.51378,7.56395 z"
+ id="path9372" />
+ <path
+ style="fill:#ffffff;fill-opacity:0.216949;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 165.96186,101.44585 195.7727,125.02756 182.64703,78.754017 Z"
+ id="path9374" />
+ </g>
+ <g
+ inkscape:groupmode="layer"
+ id="layer4"
+ inkscape:label="Vertices">
+ <circle
+ style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path27764"
+ cx="106.86934"
+ cy="142.38014"
+ r="2.0022209" />
+ <circle
+ style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="circle28773"
+ cx="111.54119"
+ cy="99.221161"
+ r="2.0022209" />
+ <circle
+ style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="circle29091"
+ cx="165.90784"
+ cy="101.36163"
+ r="2.0022209" />
+ </g>
+ </g>
+</svg>
diff --git a/src/app.rs b/src/app.rs
@@ -903,7 +903,7 @@ fn render_damus_mobile(ctx: &egui::Context, app: &mut Damus) {
main_panel(&ctx.style(), ui::is_narrow(ctx)).show(ctx, |ui| {
if !app.columns.columns().is_empty() {
- nav::render_nav(false, 0, app, ui);
+ nav::render_nav(0, app, ui);
}
});
}
@@ -951,13 +951,18 @@ fn render_damus_desktop(ctx: &egui::Context, app: &mut Damus) {
fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, columns: usize) {
StripBuilder::new(ui)
- .size(Size::exact(40.0))
+ .size(Size::exact(ui::side_panel::SIDE_PANEL_WIDTH))
.sizes(sizes, columns)
.clip(true)
.horizontal(|mut strip| {
strip.cell(|ui| {
let rect = ui.available_rect_before_wrap();
- let side_panel = DesktopSidePanel::new(app).show(ui);
+ let side_panel = DesktopSidePanel::new(
+ &app.ndb,
+ &mut app.img_cache,
+ app.accounts.get_selected_account(),
+ )
+ .show(ui);
let router = if let Some(router) = app
.columns
@@ -986,24 +991,10 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, columns: usiz
});
let n_cols = app.columns.columns().len();
- let mut first = true;
for column_ind in 0..n_cols {
strip.cell(|ui| {
let rect = ui.available_rect_before_wrap();
- let show_postbox = first
- && app
- .columns
- .column(column_ind)
- .router()
- .routes()
- .iter()
- .find_map(|r| r.timeline_id())
- .is_some();
- if show_postbox {
- first = false
- }
-
- nav::render_nav(show_postbox, column_ind, app, ui);
+ nav::render_nav(column_ind, app, ui);
// vertical line
ui.painter().vline(
diff --git a/src/nav.rs b/src/nav.rs
@@ -4,13 +4,13 @@ use crate::{
route::Route,
thread::thread_unsubscribe,
timeline::route::{render_timeline_route, TimelineRoute, TimelineRouteResponse},
- ui::{note::PostAction, RelayView, View},
+ ui::{self, note::PostAction, RelayView, View},
Damus,
};
use egui_nav::{Nav, NavAction};
-pub fn render_nav(show_postbox: bool, col: usize, app: &mut Damus, ui: &mut egui::Ui) {
+pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) {
// TODO(jb55): clean up this router_mut mess by using Router<R> in egui-nav directly
let nav_response = Nav::new(app.columns().column(col).router().routes().clone())
.navigating(app.columns_mut().column_mut(col).router_mut().navigating)
@@ -28,7 +28,6 @@ pub fn render_nav(show_postbox: bool, col: usize, app: &mut Damus, ui: &mut egui
&mut app.accounts,
*tlr,
col,
- show_postbox,
app.textmode,
ui,
),
@@ -50,6 +49,29 @@ pub fn render_nav(show_postbox: bool, col: usize, app: &mut Damus, ui: &mut egui
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.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)
+ });
+ }
+
+ None
+ }
});
if let Some(reply_response) = nav_response.inner {
diff --git a/src/route.rs b/src/route.rs
@@ -12,6 +12,7 @@ pub enum Route {
Timeline(TimelineRoute),
Accounts(AccountsRoute),
Relays,
+ ComposeNote,
}
impl Route {
@@ -123,6 +124,7 @@ impl fmt::Display for Route {
AccountsRoute::Accounts => write!(f, "Accounts"),
AccountsRoute::AddAccount => write!(f, "Add Account"),
},
+ Route::ComposeNote => write!(f, "Compose Note"),
}
}
}
diff --git a/src/timeline/route.rs b/src/timeline/route.rs
@@ -48,23 +48,11 @@ pub fn render_timeline_route(
accounts: &mut AccountManager,
route: TimelineRoute,
col: usize,
- show_postbox: bool,
textmode: bool,
ui: &mut egui::Ui,
) -> Option<TimelineRouteResponse> {
match route {
TimelineRoute::Timeline(timeline_id) => {
- if show_postbox {
- let kp = accounts.selected_or_first_nsec()?;
- let draft = drafts.compose_mut();
- let response =
- ui::timeline::postbox_view(ndb, kp, draft, img_cache, note_cache, ui);
-
- if let Some(action) = response.action {
- PostAction::execute(kp, &action, pool, draft, |np, seckey| np.to_note(seckey));
- }
- }
-
if let Some(bar_action) =
ui::TimelineView::new(timeline_id, columns, ndb, note_cache, img_cache, textmode)
.ui(ui)
diff --git a/src/ui/account_management.rs b/src/ui/account_management.rs
@@ -10,8 +10,6 @@ use egui::{Align, Button, Frame, Image, InnerResponse, Layout, RichText, ScrollA
use nostrdb::{Ndb, Transaction};
use super::profile::preview::SimpleProfilePreview;
-use super::profile::ProfilePreviewOp;
-use super::profile_preview_controller::profile_preview_view;
pub struct AccountsView<'a> {
ndb: &'a Ndb,
@@ -26,6 +24,12 @@ pub enum AccountsViewResponse {
RouteToLogin,
}
+#[derive(Debug)]
+enum ProfilePreviewOp {
+ RemoveAccount,
+ SwitchTo,
+}
+
impl<'a> AccountsView<'a> {
pub fn new(ndb: &'a Ndb, accounts: &'a AccountManager, img_cache: &'a mut ImageCache) -> Self {
AccountsView {
@@ -86,9 +90,13 @@ impl<'a> AccountsView<'a> {
false
};
- if let Some(op) =
- profile_preview_view(ui, profile.as_ref(), img_cache, is_selected)
- {
+ let profile_peview_view = {
+ let width = ui.available_width();
+ let preview = SimpleProfilePreview::new(profile.as_ref(), img_cache);
+ show_profile_card(ui, preview, width, is_selected)
+ };
+
+ if let Some(op) = profile_peview_view {
return_op = Some(match op {
ProfilePreviewOp::SwitchTo => AccountsViewResponse::SelectAccount(i),
ProfilePreviewOp::RemoveAccount => {
@@ -119,7 +127,7 @@ impl<'a> AccountsView<'a> {
}
}
-pub fn show_profile_card(
+fn show_profile_card(
ui: &mut egui::Ui,
preview: SimpleProfilePreview,
width: f32,
diff --git a/src/ui/anim.rs b/src/ui/anim.rs
@@ -1,3 +1,5 @@
+use egui::{Pos2, Rect, Response, Sense};
+
pub fn hover_expand(
ui: &mut egui::Ui,
id: egui::Id,
@@ -25,3 +27,70 @@ pub fn hover_expand_small(ui: &mut egui::Ui, id: egui::Id) -> (egui::Rect, f32,
hover_expand(ui, id, size, expand_size, anim_speed)
}
+
+pub static ICON_EXPANSION_MULTIPLE: f32 = 1.2;
+pub static ANIM_SPEED: f32 = 0.05;
+pub struct AnimationHelper {
+ rect: Rect,
+ center: Pos2,
+ response: Response,
+ animation_progress: f32,
+ expansion_multiple: f32,
+}
+
+impl AnimationHelper {
+ pub fn new(
+ ui: &mut egui::Ui,
+ animation_name: impl std::hash::Hash,
+ max_size: egui::Vec2,
+ ) -> Self {
+ let id = ui.id().with(animation_name);
+ let (rect, response) = ui.allocate_exact_size(max_size, Sense::click());
+
+ let animation_progress =
+ ui.ctx()
+ .animate_bool_with_time(id, response.hovered(), ANIM_SPEED);
+
+ Self {
+ rect,
+ center: 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;
+
+ if self.response.is_pointer_button_down_on() {
+ min_object_size
+ } else {
+ min_object_size + ((max_object_size - min_object_size) * self.animation_progress)
+ }
+ }
+
+ pub fn scale_radius(&self, min_diameter: f32) -> f32 {
+ self.scale_1d_pos((min_diameter - 1.0) / 2.0)
+ }
+
+ pub fn get_animation_rect(&self) -> egui::Rect {
+ self.rect
+ }
+
+ pub fn center(&self) -> Pos2 {
+ self.rect.center()
+ }
+
+ pub fn take_animation_response(self) -> egui::Response {
+ self.response
+ }
+
+ // Scale a minimum position from center to the current animation position
+ pub fn scale_from_center(&self, x_min: f32, y_min: f32) -> Pos2 {
+ Pos2::new(
+ self.center.x + self.scale_1d_pos(x_min),
+ self.center.y + self.scale_1d_pos(y_min),
+ )
+ }
+}
diff --git a/src/ui/mod.rs b/src/ui/mod.rs
@@ -15,7 +15,7 @@ pub use account_management::AccountsView;
pub use mention::Mention;
pub use note::{NoteResponse, NoteView, PostReplyView, PostView};
pub use preview::{Preview, PreviewApp, PreviewConfig};
-pub use profile::{profile_preview_controller, ProfilePic, ProfilePreview};
+pub use profile::{ProfilePic, ProfilePreview};
pub use relay::RelayView;
pub use side_panel::{DesktopSidePanel, SidePanelAction};
pub use thread::ThreadView;
diff --git a/src/ui/profile/mod.rs b/src/ui/profile/mod.rs
@@ -1,7 +1,5 @@
pub mod picture;
pub mod preview;
-pub mod profile_preview_controller;
pub use picture::ProfilePic;
pub use preview::ProfilePreview;
-pub use profile_preview_controller::ProfilePreviewOp;
diff --git a/src/ui/profile/preview.rs b/src/ui/profile/preview.rs
@@ -1,6 +1,7 @@
use crate::app_style::NotedeckTextStyle;
use crate::imgcache::ImageCache;
use crate::ui::ProfilePic;
+use crate::user_account::UserAccount;
use crate::{colors, images, DisplayName};
use egui::load::TexturePoll;
use egui::{Frame, RichText, Sense, Widget};
@@ -167,6 +168,30 @@ 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()
+ }
+}
+
+pub fn get_account_url<'a>(
+ txn: &'a nostrdb::Transaction,
+ ndb: &nostrdb::Ndb,
+ account: Option<&UserAccount>,
+) -> &'a str {
+ if let Some(selected_account) = account {
+ if let Ok(profile) = ndb.get_profile_by_pubkey(txn, selected_account.pubkey.bytes()) {
+ get_profile_url_owned(Some(profile))
+ } else {
+ get_profile_url_owned(None)
+ }
+ } else {
+ get_profile_url(None)
+ }
+}
+
fn display_name_widget(
display_name: DisplayName<'_>,
add_placeholder_space: bool,
diff --git a/src/ui/profile/profile_preview_controller.rs b/src/ui/profile/profile_preview_controller.rs
@@ -1,126 +0,0 @@
-use egui::Ui;
-use nostrdb::{Ndb, ProfileRecord, Transaction};
-
-use crate::{
- imgcache::ImageCache, ui::account_management::show_profile_card, Damus, DisplayName, Result,
-};
-
-use super::{
- preview::{get_display_name, get_profile_url, SimpleProfilePreview},
- ProfilePic,
-};
-
-#[derive(Debug)]
-pub enum ProfilePreviewOp {
- RemoveAccount,
- SwitchTo,
-}
-
-pub fn profile_preview_view(
- ui: &mut Ui,
- profile: Option<&'_ ProfileRecord<'_>>,
- img_cache: &mut ImageCache,
- is_selected: bool,
-) -> Option<ProfilePreviewOp> {
- let width = ui.available_width();
-
- let preview = SimpleProfilePreview::new(profile, img_cache);
- show_profile_card(ui, preview, width, is_selected)
-}
-
-pub fn view_profile_previews(
- app: &mut Damus,
- ui: &mut egui::Ui,
- add_preview_ui: fn(
- ui: &mut egui::Ui,
- preview: SimpleProfilePreview,
- width: f32,
- is_selected: bool,
- index: usize,
- ) -> bool,
-) -> Option<usize> {
- let width = ui.available_width();
-
- let txn = if let Ok(txn) = Transaction::new(app.ndb()) {
- txn
- } else {
- return None;
- };
-
- for i in 0..app.accounts().num_accounts() {
- let account = if let Some(account) = app.accounts().get_account(i) {
- account
- } else {
- continue;
- };
-
- let profile = app
- .ndb()
- .get_profile_by_pubkey(&txn, account.pubkey.bytes())
- .ok();
-
- let is_selected = if let Some(selected) = app.accounts().get_selected_account_index() {
- i == selected
- } else {
- false
- };
-
- let preview = SimpleProfilePreview::new(profile.as_ref(), app.img_cache_mut());
-
- if add_preview_ui(ui, preview, width, is_selected, i) {
- return Some(i);
- }
- }
-
- None
-}
-
-pub fn show_with_nickname(
- ndb: &Ndb,
- ui: &mut egui::Ui,
- key: &[u8; 32],
- ui_element: fn(ui: &mut egui::Ui, username: &DisplayName) -> egui::Response,
-) -> Result<egui::Response> {
- let txn = Transaction::new(ndb)?;
- let profile = ndb.get_profile_by_pubkey(&txn, key)?;
- Ok(ui_element(ui, &get_display_name(Some(&profile))))
-}
-
-pub fn show_with_selected_pfp(
- app: &mut Damus,
- ui: &mut egui::Ui,
- ui_element: fn(ui: &mut egui::Ui, pfp: ProfilePic) -> egui::Response,
-) -> Option<egui::Response> {
- let selected_account = app.accounts().get_selected_account();
- if let Some(selected_account) = selected_account {
- if let Ok(txn) = Transaction::new(app.ndb()) {
- let profile = app
- .ndb()
- .get_profile_by_pubkey(&txn, selected_account.pubkey.bytes());
-
- return Some(ui_element(
- ui,
- ProfilePic::new(app.img_cache_mut(), get_profile_url(profile.ok().as_ref())),
- ));
- }
- }
-
- None
-}
-
-pub fn show_with_pfp(
- app: &mut Damus,
- ui: &mut egui::Ui,
- key: &[u8; 32],
- ui_element: fn(ui: &mut egui::Ui, pfp: ProfilePic) -> egui::Response,
-) -> Option<egui::Response> {
- if let Ok(txn) = Transaction::new(app.ndb()) {
- let profile = app.ndb().get_profile_by_pubkey(&txn, key);
-
- return Some(ui_element(
- ui,
- ProfilePic::new(app.img_cache_mut(), get_profile_url(profile.ok().as_ref())),
- ));
- }
- None
-}
diff --git a/src/ui/side_panel.rs b/src/ui/side_panel.rs
@@ -1,17 +1,29 @@
-use egui::{Button, Layout, SidePanel, Vec2, Widget};
+use egui::{vec2, Color32, InnerResponse, Layout, Margin, Separator, SidePanel, Stroke, Widget};
+use tracing::info;
use crate::{
account_manager::AccountsRoute,
+ colors,
column::Column,
+ imgcache::ImageCache,
route::{Route, Router},
- ui::profile_preview_controller,
+ user_account::UserAccount,
Damus,
};
-use super::{ProfilePic, View};
+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> {
- app: &'a mut Damus,
+ ndb: &'a nostrdb::Ndb,
+ img_cache: &'a mut ImageCache,
+ selected_account: Option<&'a UserAccount>,
}
impl<'a> View for DesktopSidePanel<'a> {
@@ -26,6 +38,9 @@ pub enum SidePanelAction {
Account,
Settings,
Columns,
+ ComposeNote,
+ Search,
+ ExpandSidePanel,
}
pub struct SidePanelResponse {
@@ -40,33 +55,93 @@ impl SidePanelResponse {
}
impl<'a> DesktopSidePanel<'a> {
- pub fn new(app: &'a mut Damus) -> Self {
- DesktopSidePanel { app }
+ 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 panel() -> SidePanel {
egui::SidePanel::left("side_panel")
.resizable(false)
- .exact_width(40.0)
+ .exact_width(SIDE_PANEL_WIDTH)
}
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 spacing_amt = 16.0;
let inner = 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 settings_resp = ui.add(settings_button(dark_mode));
- let column_resp = ui.add(add_column_button(dark_mode));
-
- if pfp_resp.clicked() {
- egui::InnerResponse::new(SidePanelAction::Account, pfp_resp)
- } else if settings_resp.clicked() || settings_resp.hovered() {
- egui::InnerResponse::new(SidePanelAction::Settings, settings_resp)
- } else if column_resp.clicked() || column_resp.hovered() {
- egui::InnerResponse::new(SidePanelAction::Columns, column_resp)
+ .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 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 {
+ 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)
}
@@ -77,13 +152,20 @@ impl<'a> DesktopSidePanel<'a> {
}
fn pfp_button(&mut self, ui: &mut egui::Ui) -> egui::Response {
- if let Some(resp) =
- profile_preview_controller::show_with_selected_pfp(self.app, ui, show_pfp())
- {
- resp
- } else {
- add_button_to_ui(ui, no_account_pfp())
- }
+ 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(router: &mut Router<Route>, action: SidePanelAction) {
@@ -109,40 +191,167 @@ impl<'a> DesktopSidePanel<'a> {
router.route_to(Route::relays());
}
}
- SidePanelAction::Columns => (), // TODO
+ SidePanelAction::Columns => {
+ // TODO
+ info!("Clicked columns button");
+ }
+ 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");
+ }
}
}
}
-fn show_pfp() -> fn(ui: &mut egui::Ui, pfp: ProfilePic) -> egui::Response {
- |ui, pfp| {
- let response = pfp.ui(ui);
- ui.allocate_rect(response.rect, egui::Sense::click())
+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 settings_button(dark_mode: bool) -> egui::Button<'static> {
+fn add_column_button(dark_mode: bool) -> impl Widget {
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 {
- ui.add_sized(Vec2::new(32.0, 32.0), button)
+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 no_account_pfp() -> Button<'static> {
- Button::new("A")
- .rounding(20.0)
- .min_size(Vec2::new(38.0, 38.0))
+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()
+ }
}
-fn add_column_button(dark_mode: bool) -> egui::Button<'static> {
- let _ = dark_mode;
- let img_data = egui::include_image!("../../assets/icons/add_column_dark_4x.png");
+// 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.svg");
+ let img = egui::Image::new(img_data).max_width(img_size);
- egui::Button::image(egui::Image::new(img_data).max_width(32.0)).frame(false)
+ ui.add(img)
+ }
}
mod preview {
@@ -173,12 +382,16 @@ mod preview {
impl View for DesktopSidePanelPreview {
fn ui(&mut self, ui: &mut egui::Ui) {
StripBuilder::new(ui)
- .size(Size::exact(40.0))
+ .size(Size::exact(SIDE_PANEL_WIDTH))
.sizes(Size::remainder(), 0)
.clip(true)
.horizontal(|mut strip| {
strip.cell(|ui| {
- let mut panel = DesktopSidePanel::new(&mut self.app);
+ 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(
diff --git a/src/ui/timeline.rs b/src/ui/timeline.rs
@@ -1,4 +1,3 @@
-use crate::draft::Draft;
use crate::{
actionbar::BarAction, column::Columns, imgcache::ImageCache, notecache::NoteCache,
timeline::TimelineId, ui,
@@ -6,12 +5,9 @@ use crate::{
use egui::containers::scroll_area::ScrollBarVisibility;
use egui::{Direction, Layout};
use egui_tabs::TabColor;
-use enostr::FilledKeypair;
use nostrdb::{Ndb, Transaction};
use tracing::{debug, error, warn};
-use super::note::PostResponse;
-
pub struct TimelineView<'a> {
timeline_id: TimelineId,
columns: &'a mut Columns,
@@ -175,27 +171,6 @@ fn timeline_ui(
bar_action
}
-pub fn postbox_view<'a>(
- ndb: &'a Ndb,
- key: FilledKeypair<'a>,
- draft: &'a mut Draft,
- img_cache: &'a mut ImageCache,
- note_cache: &'a mut NoteCache,
- ui: &'a mut egui::Ui,
-) -> PostResponse {
- // show a postbox in the first timeline
- let txn = Transaction::new(ndb).expect("txn");
- ui::PostView::new(
- ndb,
- draft,
- crate::draft::DraftSource::Compose,
- img_cache,
- note_cache,
- key,
- )
- .ui(&txn, ui)
-}
-
fn tabs_ui(ui: &mut egui::Ui) -> i32 {
ui.spacing_mut().item_spacing.y = 0.0;