commit 5b0068e6cb5ce5603c2f97b2384d9c676bb00de8
parent 2cbae68a7f1425997f66d5ac0662f5cfde172f4f
Author: kernelkind <kernelkind@gmail.com>
Date: Sat, 25 Jan 2025 15:32:39 -0500
add more notes indicator
closes: https://github.com/damus-io/notedeck/issues/72
Signed-off-by: kernelkind <kernelkind@gmail.com>
Diffstat:
1 file changed, 115 insertions(+), 26 deletions(-)
diff --git a/crates/notedeck_columns/src/ui/timeline.rs b/crates/notedeck_columns/src/ui/timeline.rs
@@ -1,3 +1,5 @@
+use std::f32::consts::PI;
+
use crate::actionbar::NoteAction;
use crate::timeline::TimelineTab;
use crate::{
@@ -7,13 +9,15 @@ use crate::{
ui::note::NoteOptions,
};
use egui::containers::scroll_area::ScrollBarVisibility;
-use egui::{Direction, Layout};
+use egui::{vec2, Direction, Layout, Pos2, Stroke};
use egui_tabs::TabColor;
use nostrdb::{Ndb, Transaction};
use notedeck::note::root_note_id_from_selected_id;
use notedeck::{ImageCache, MuteFun, NoteCache};
use tracing::{error, warn};
+use super::anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE};
+
pub struct TimelineView<'a> {
timeline_id: TimelineId,
columns: &'a mut Columns,
@@ -105,35 +109,120 @@ fn timeline_ui(
egui::Id::new(("tlscroll", timeline.view_id()))
};
- egui::ScrollArea::vertical()
+ let show_top_button_id = ui.id().with((scroll_id, "at_top"));
+
+ let show_top_button = ui
+ .ctx()
+ .data(|d| d.get_temp::<bool>(show_top_button_id))
+ .unwrap_or(false);
+
+ let goto_top = if show_top_button {
+ let top_button_pos = ui.available_rect_before_wrap().right_top() - vec2(48.0, -24.0);
+ egui::Area::new(ui.id().with("foreground_area"))
+ .order(egui::Order::Foreground)
+ .fixed_pos(top_button_pos)
+ .show(ui.ctx(), |ui| {
+ ui.add(goto_top_button(top_button_pos)).clicked()
+ })
+ .inner
+ } else {
+ false
+ };
+
+ let mut scroll_area = egui::ScrollArea::vertical()
.id_salt(scroll_id)
.animated(false)
.auto_shrink([false, false])
- .scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible)
- .show(ui, |ui| {
- let timeline = if let Some(timeline) = columns.find_timeline_mut(timeline_id) {
- timeline
- } else {
- 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;
- };
+ .scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible);
+ if goto_top {
+ scroll_area = scroll_area.vertical_scroll_offset(0.0);
+ }
- let txn = Transaction::new(ndb).expect("failed to create txn");
- TimelineTabView::new(
- timeline.current_view(),
- reversed,
- note_options,
- &txn,
- ndb,
- note_cache,
- img_cache,
- is_muted,
- )
- .show(ui)
- })
- .inner
+ let scroll_output = scroll_area.show(ui, |ui| {
+ let timeline = if let Some(timeline) = columns.find_timeline_mut(timeline_id) {
+ timeline
+ } else {
+ 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;
+ };
+
+ let txn = Transaction::new(ndb).expect("failed to create txn");
+ TimelineTabView::new(
+ timeline.current_view(),
+ reversed,
+ note_options,
+ &txn,
+ ndb,
+ note_cache,
+ img_cache,
+ is_muted,
+ )
+ .show(ui)
+ });
+
+ let at_top_after_scroll = scroll_output.state.offset.y == 0.0;
+ let cur_show_top_button = ui.ctx().data(|d| d.get_temp::<bool>(show_top_button_id));
+
+ if at_top_after_scroll {
+ if cur_show_top_button != Some(false) {
+ ui.ctx()
+ .data_mut(|d| d.insert_temp(show_top_button_id, false));
+ }
+ } else if cur_show_top_button == Some(false) {
+ ui.ctx()
+ .data_mut(|d| d.insert_temp(show_top_button_id, true));
+ }
+
+ scroll_output.inner
+}
+
+fn goto_top_button(center: Pos2) -> impl egui::Widget {
+ move |ui: &mut egui::Ui| -> egui::Response {
+ let radius = 12.0;
+ let max_size = vec2(
+ ICON_EXPANSION_MULTIPLE * 2.0 * radius,
+ ICON_EXPANSION_MULTIPLE * 2.0 * radius,
+ );
+ let helper = AnimationHelper::new_from_rect(ui, "goto_top", {
+ let painter = ui.painter();
+ let center = painter.round_pos_to_pixel_center(center);
+ egui::Rect::from_center_size(center, max_size)
+ });
+
+ let painter = ui.painter();
+ painter.circle_filled(center, helper.scale_1d_pos(radius), crate::colors::PINK);
+
+ let create_pt = |angle: f32| {
+ let side = radius / 2.0;
+ let x = side * angle.cos();
+ let mut y = side * angle.sin();
+
+ let height = (side * (3.0_f32).sqrt()) / 2.0;
+ y += height / 2.0;
+ Pos2 { x, y }
+ };
+
+ let left_pt =
+ painter.round_pos_to_pixel_center(helper.scale_pos_from_center(create_pt(-PI)));
+ let center_pt =
+ painter.round_pos_to_pixel_center(helper.scale_pos_from_center(create_pt(-PI / 2.0)));
+ let right_pt =
+ painter.round_pos_to_pixel_center(helper.scale_pos_from_center(create_pt(0.0)));
+
+ let line_width = helper.scale_1d_pos(4.0);
+ let line_color = ui.visuals().text_color();
+ painter.line_segment([left_pt, center_pt], Stroke::new(line_width, line_color));
+ painter.line_segment([center_pt, right_pt], Stroke::new(line_width, line_color));
+
+ let end_radius = (line_width - 1.0) / 2.0;
+ painter.circle_filled(left_pt, end_radius, line_color);
+ painter.circle_filled(center_pt, end_radius, line_color);
+ painter.circle_filled(right_pt, end_radius, line_color);
+
+ helper.take_animation_response()
+ }
}
pub fn tabs_ui(ui: &mut egui::Ui, selected: usize, views: &[TimelineTab]) -> usize {