notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 9e8f7a2e5c64d2e553fd0ddf436cd79d0a11544f
parent 029896627cf83c0ea1dec1d618500c4ddebbe8f8
Author: William Casarin <jb55@jb55.com>
Date:   Wed, 15 May 2024 17:20:25 -0700

ui: integrate egui-tabs for notes & replies selector

demo: https://cdn.jb55.com/s/notedeck-tabs.mp4

Fixes: https://github.com/damus-io/notedeck/issues/47
Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
MCargo.lock | 10++++++++++
MCargo.toml | 1+
Msrc/app.rs | 6+++---
Msrc/app_creation.rs | 4++++
Msrc/colors.rs | 3++-
Msrc/timeline.rs | 94++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
6 files changed, 108 insertions(+), 10 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1036,6 +1036,15 @@ dependencies = [ ] [[package]] +name = "egui-tabs" +version = "0.1.0" +source = "git+https://github.com/damus-io/egui-tabs?rev=ed97a57fc66b3781bc10ab644f9e1ed125d7377a#ed97a57fc66b3781bc10ab644f9e1ed125d7377a" +dependencies = [ + "egui", + "egui_extras", +] + +[[package]] name = "egui-wgpu" version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2445,6 +2454,7 @@ dependencies = [ "console_error_panic_hook", "eframe", "egui", + "egui-tabs", "egui_extras", "egui_virtual_list", "ehttp 0.2.0", diff --git a/Cargo.toml b/Cargo.toml @@ -19,6 +19,7 @@ eframe = { version = "0.27.2", default-features = false, features = [ "glow", "w #eframe = "0.22.0" egui_extras = { version = "0.27.2", features = ["all_loaders"] } ehttp = "0.2.0" +egui-tabs = { git = "https://github.com/damus-io/egui-tabs", rev = "ed97a57fc66b3781bc10ab644f9e1ed125d7377a" } reqwest = { version = "0.12.4", default-features = false, features = [ "rustls-tls-native-roots" ] } image = { version = "0.24", features = ["jpeg", "png", "webp"] } log = "0.4.17" diff --git a/src/app.rs b/src/app.rs @@ -56,9 +56,9 @@ fn relay_setup(pool: &mut RelayPool, ctx: &egui::Context) { if let Err(e) = pool.add_url("wss://relay.damus.io".to_string(), wakeup.clone()) { error!("{:?}", e) } - if let Err(e) = pool.add_url("wss://pyramid.fiatjaf.com".to_string(), wakeup.clone()) { - error!("{:?}", e) - } + //if let Err(e) = pool.add_url("wss://pyramid.fiatjaf.com".to_string(), wakeup.clone()) { + //error!("{:?}", e) + //} if let Err(e) = pool.add_url("wss://nos.lol".to_string(), wakeup.clone()) { error!("{:?}", e) } diff --git a/src/app_creation.rs b/src/app_creation.rs @@ -41,6 +41,10 @@ pub fn setup_cc(cc: &eframe::CreationContext<'_>) { setup_fonts(ctx); //ctx.set_pixels_per_point(ctx.pixels_per_point() + UI_SCALE_FACTOR); + //ctx.set_pixels_per_point(1.0); + // + // + //ctx.tessellation_options_mut(|to| to.feathering = false); egui_extras::install_image_loaders(ctx); diff --git a/src/colors.rs b/src/colors.rs @@ -15,6 +15,7 @@ const DARK_BG: Color32 = Color32::from_rgb(0x2C, 0x2C, 0x2C); const DARK_ISH_BG: Color32 = Color32::from_rgb(0x22, 0x22, 0x22); const SEMI_DARK_BG: Color32 = Color32::from_rgb(0x44, 0x44, 0x44); +const LIGHTER_GRAY: Color32 = Color32::from_rgb(0xe8, 0xe8, 0xe8); const LIGHT_GRAY: Color32 = Color32::from_rgb(0xc8, 0xc8, 0xc8); // 78% pub const MID_GRAY: Color32 = Color32::from_rgb(0xbd, 0xbd, 0xbd); const DARKER_GRAY: Color32 = Color32::from_rgb(0xa5, 0xa5, 0xa5); // 65% @@ -99,7 +100,7 @@ pub fn light_color_theme() -> ColorTheme { // NONINTERACTIVE WIDGET noninteractive_bg_fill: Color32::WHITE, noninteractive_weak_bg_fill: EVEN_DARKER_GRAY, - noninteractive_bg_stroke_color: DARKER_GRAY, + noninteractive_bg_stroke_color: LIGHTER_GRAY, noninteractive_fg_stroke_color: GRAY_SECONDARY, // INACTIVE WIDGET diff --git a/src/timeline.rs b/src/timeline.rs @@ -1,5 +1,7 @@ use crate::{ui, Damus}; use egui::containers::scroll_area::ScrollBarVisibility; +use egui::{Direction, Layout}; +use egui_tabs::TabColor; use egui_virtual_list::VirtualList; use enostr::Filter; use nostrdb::{NoteKey, Subscription, Transaction}; @@ -56,6 +58,90 @@ impl Timeline { } } +fn get_label_width(ui: &mut egui::Ui, text: &str) -> f32 { + let font_id = egui::FontId::default(); + let galley = ui.fonts(|r| r.layout_no_wrap(text.to_string(), font_id, egui::Color32::WHITE)); + galley.rect.width() +} + +fn shrink_range_to_width(range: egui::Rangef, width: f32) -> egui::Rangef { + let midpoint = (range.min + range.max) / 2.0; + let half_width = width / 2.0; + + let min = midpoint - half_width; + let max = midpoint + half_width; + + egui::Rangef::new(min, max) +} + +fn tabs_ui(ui: &mut egui::Ui) { + ui.spacing_mut().item_spacing.y = 0.0; + + let tab_res = egui_tabs::Tabs::new(2) + .hover_bg(TabColor::none()) + .selected_fg(TabColor::none()) + .selected_bg(TabColor::none()) + .hover_bg(TabColor::none()) + //.hover_bg(TabColor::custom(egui::Color32::RED)) + .height(32.0) + .layout(Layout::centered_and_justified(Direction::TopDown)) + .show(ui, |ui, state| { + ui.spacing_mut().item_spacing.y = 0.0; + + let ind = state.index(); + + let txt = if ind == 0 { "Notes" } else { "Notes & Replies" }; + + let res = ui.add(egui::Label::new(txt).selectable(false)); + + // underline + if state.is_selected() { + let rect = res.rect; + let underline = rect.x_range().shrink(rect.width() / 4.0); + let underline = shrink_range_to_width(underline, get_label_width(ui, txt) * 1.15); + let underline_y = ui.painter().round_to_pixel(rect.bottom()) - 1.5; + return (underline, underline_y); + } + + (egui::Rangef::new(0.0, 0.0), 0.0) + }); + + //ui.add_space(0.5); + ui::hline(ui); + + // fun animation + if let Some(sel) = tab_res.selected() { + let (underline, underline_y) = tab_res.inner()[sel as usize].inner; + let underline_width = underline.span(); + + let tab_anim_id = ui.id().with("tab_anim"); + let tab_anim_size = tab_anim_id.with("size"); + + let stroke = egui::Stroke { + color: ui.visuals().hyperlink_color, + width: 3.0, + }; + + let speed = 0.1f32; + + // animate underline position + let x = ui + .ctx() + .animate_value_with_time(tab_anim_id, underline.min, speed); + + // animate underline width + let w = ui + .ctx() + .animate_value_with_time(tab_anim_size, underline_width, speed); + + let underline = egui::Rangef::new(x, x + w); + + ui.painter().hline(underline, underline_y, stroke); + } + + ui.add_space(3.0); +} + pub fn timeline_view(ui: &mut egui::Ui, app: &mut Damus, timeline: usize) { //padding(4.0, ui, |ui| ui.heading("Notifications")); /* @@ -63,14 +149,10 @@ pub fn timeline_view(ui: &mut egui::Ui, app: &mut Damus, timeline: usize) { let row_height = ui.fonts(|f| f.row_height(&font_id)) + ui.spacing().item_spacing.y; */ + tabs_ui(ui); + egui::ScrollArea::vertical() .scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible) - //.auto_shrink([false; 2]) - /* - .show_viewport(ui, |ui, viewport| { - render_notes_in_viewport(ui, app, viewport, row_height, font_id); - }); - */ .show(ui, |ui| { let len = app.timelines[timeline].notes.len(); let list = app.timelines[timeline].list.clone();