notedeck

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

commit d617b688f1a0f7e8874f71916ad2846b7b304bef
parent 0d51e25ab0cfaebb219a7dafd90c0408f500086c
Author: William Casarin <jb55@jb55.com>
Date:   Mon, 21 Apr 2025 13:10:20 -0700

docs: add some ui-related guides

generated using code2prompt + claude 3.7 sonnet

Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Acrates/notedeck_ui/README.md | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/notedeck_ui/docs/README.md | 237+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/notedeck_ui/docs/components.md | 321+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 679 insertions(+), 0 deletions(-)

diff --git a/crates/notedeck_ui/README.md b/crates/notedeck_ui/README.md @@ -0,0 +1,121 @@ +# NoteDeck UI + +UI component library for NoteDeck - a Nostr client built with EGUI. + +## Overview + +The `notedeck_ui` crate provides a set of reusable UI components for building a Nostr client. It offers consistent styling, behavior, and rendering of Nostr-specific elements like notes, profiles, mentions, and media content. + +This library is built on top of [egui](https://github.com/emilk/egui), a simple, fast, and highly portable immediate mode GUI library for Rust. + +## Features + +- 📝 Note display with rich content, media, and interactions +- 👤 Profile components (display name, pictures, banners) +- 🔗 Mention system with hover previews +- 🖼️ Image handling with caching and lazy loading +- 📺 GIF playback support +- 💸 Zap interactions (Bitcoin Lightning tips) +- 🎨 Theming and consistent styling + +## Components + +### Notes + +The `NoteView` widget is the core component for displaying Nostr notes: + +```rust +// Example: Render a note +let mut note_view = NoteView::new( + note_context, + current_account, + &note, + NoteOptions::default() +); + +ui.add(&mut note_view); +``` + +`NoteView` supports various display options: + +```rust +// Create a preview style note +note_view + .preview_style() // Apply preview styling + .textmode(true) // Use text-only mode + .actionbar(false) // Hide action bar + .small_pfp(true) // Use small profile picture + .note_previews(false) // Disable nested note previews + .show(ui); +``` + +### Profiles + +Profile components include profile pictures, banners, and display names: + +```rust +// Display a profile picture +ui.add(ProfilePic::new(images_cache, profile_picture_url).size(48.0)); + +// Display a profile preview +ui.add(ProfilePreview::new(profile_record, images_cache)); +``` + +### Mentions + +The mention component links to user profiles: + +```rust +// Display a mention with hover preview +let mention_response = Mention::new(ndb, img_cache, txn, pubkey) + .size(16.0) // Set text size + .selectable(true) // Allow selection + .show(ui); + +// Handle click actions +if let Some(action) = mention_response.inner { + // Handle profile navigation +} +``` + +### Media + +Support for images, GIFs, and other media types: + +```rust +// Render an image +render_images( + ui, + img_cache, + image_url, + ImageType::Content, + cache_type, + on_loading_callback, + on_error_callback, + on_success_callback +); +``` + +## Styling + +The UI components adapt to the current theme (light/dark mode) and use consistent styling defined in the `colors.rs` module: + +```rust +// Color constants +pub const ALMOST_WHITE: Color32 = Color32::from_rgb(0xFA, 0xFA, 0xFA); +pub const MID_GRAY: Color32 = Color32::from_rgb(0xbd, 0xbd, 0xbd); +pub const PINK: Color32 = Color32::from_rgb(0xE4, 0x5A, 0xC9); +pub const TEAL: Color32 = Color32::from_rgb(0x77, 0xDC, 0xE1); +``` + +## Dependencies + +This crate depends on: +- `egui` - Core UI library +- `egui_extras` - Additional widgets and functionality +- `ehttp` - HTTP client for fetching content +- `nostrdb` - Nostr database and types +- `enostr` - Nostr protocol implementation +- `image` - Image processing library +- `poll-promise` - Async promise handling +- `tokio` - Async runtime diff --git a/crates/notedeck_ui/docs/README.md b/crates/notedeck_ui/docs/README.md @@ -0,0 +1,237 @@ +# NoteDeck UI Developer Documentation + +This document provides an in-depth overview of the `notedeck_ui` architecture, components, and guidelines for development. + +For a guide on some of our components, check out the [NoteDeck UI Component Guide](./components.md) + +## Architecture + +The `notedeck_ui` crate is organized into modules that handle different aspects of the Nostr client UI: + +``` +notedeck_ui +├── anim.rs - Animation utilities and helpers +├── colors.rs - Color constants and theme definitions +├── constants.rs - UI constants (margins, sizes, etc.) +├── gif.rs - GIF rendering and playback +├── icons.rs - Icon rendering helpers +├── images.rs - Image loading, caching, and display +├── lib.rs - Main export and shared utilities +├── mention.rs - Nostr mention component (@username) +├── note/ - Note display components +│ ├── contents.rs - Note content rendering +│ ├── context.rs - Note context menu +│ ├── mod.rs - Note view component +│ ├── options.rs - Note display options +│ └── reply_description.rs - Reply metadata display +├── profile/ - Profile components +│ ├── mod.rs - Shared profile utilities +│ ├── name.rs - Profile name display +│ ├── picture.rs - Profile picture component +│ └── preview.rs - Profile hover preview +├── username.rs - Username display component +└── widgets.rs - Generic widget helpers +``` + +## Core Components + +### NoteView + +The `NoteView` component is the primary way to display Nostr notes. It handles rendering the note content, profile information, media, and interactive elements like replies and zaps. + +Key design aspects: +- Stateful widget that maintains rendering state through EGUI's widget system +- Configurable display options via `NoteOptions` bitflags +- Support for different layouts (normal and wide) +- Handles nested content (note previews, mentions, hashtags) + +```rust +// NoteView creation and display +let mut note_view = NoteView::new(note_context, cur_acc, &note, options); +note_view.show(ui); // Returns NoteResponse with action +``` + +### Note Actions + +The note components use a pattern where user interactions produce `NoteAction` enum values: + +```rust +pub enum NoteAction { + Note(NoteId), // Note was clicked + Profile(Pubkey), // Profile was clicked + Reply(NoteId), // Reply button clicked + Quote(NoteId), // Quote button clicked + Hashtag(String), // Hashtag was clicked + Zap(ZapAction), // Zap interaction + Context(ContextSelection), // Context menu selection +} +``` + +Actions are propagated up from inner components to the parent UI, which can handle navigation and state changes. + +### Media Handling + +The media system uses a cache and promise-based loading system: + +1. `MediaCache` stores loaded images and animations +2. `fetch_img` retrieves images from disk or network +3. `render_images` handles the loading states and display + +For GIFs, the system: +1. Decodes frames using a background thread +2. Sends frames via channels to the UI thread +3. Manages animation timing for playback + +## Design Patterns + +### Widget Pattern + +Components implement the `egui::Widget` trait for integration with EGUI: + +```rust +impl egui::Widget for ProfilePic<'_, '_> { + fn ui(self, ui: &mut egui::Ui) -> egui::Response { + render_pfp(ui, self.cache, self.url, self.size, self.border) + } +} +``` + +### Builder Pattern + +Components often use a builder pattern for configuration: + +```rust +let view = NoteView::new(context, account, &note, options) + .small_pfp(true) + .wide(true) + .actionbar(false); +``` + +### Animation Helper + +For interactive elements, the `AnimationHelper` provides standardized hover animations: + +```rust +let helper = AnimationHelper::new(ui, "animation_id", max_size); +let current_size = helper.scale_1d_pos(min_size); +``` + +## Working with Images + +Images are handled through different types depending on their purpose: + +1. `ImageType::Profile` - For profile pictures (with automatic cropping and rounding) +2. `ImageType::Content` - For general content images + +```rust +// Loading and displaying an image +render_images( + ui, + img_cache, + url, + ImageType::Profile(128), // Size hint + MediaCacheType::Image, // Static image or GIF + |ui| { /* show while loading */ }, + |ui, err| { /* show on error */ }, + |ui, url, img, gifs| { /* show successful load */ }, +); +``` + +## Performance Considerations + +1. **Image Caching**: Images are cached both in memory and on disk +2. **Animation Optimization**: GIF frames are decoded in background threads +3. **Render Profiling**: Critical paths use `#[profiling::function]` for tracing +4. **Layout Reuse**: Components cache layout data to prevent recalculation + +## Theming + +The UI adapts to light/dark mode through EGUI's visuals system: + +```rust +// Access current theme +let color = ui.visuals().hyperlink_color; + +// Check theme mode +if ui.visuals().dark_mode { + // Use dark mode resources +} else { + // Use light mode resources +} +``` + +## Testing Components + +To test UI components, use the EGUI test infrastructure: + +```rust +#[test] +fn test_profile_pic() { + let ctx = egui::Context::default(); + let mut cache = Images::new(); + + ctx.run(|ctx| { + egui::CentralPanel::default().show(ctx, |ui| { + let response = ui.add(ProfilePic::new(&mut cache, "test_url")); + assert!(response.clicked() == false); + }); + }); +} +``` + +## Debugging Tips + +1. **EGUI Inspector**: Use `ctx.debug_painter()` to visualize layout bounds +2. **Trace Logging**: Enable trace logs to debug image loading and caching +3. **Animation Debugging**: Set `ANIM_SPEED` to a lower value to slow animations for visual debugging +4. **ID Collisions**: Use unique IDs for animations and state to prevent interaction bugs + +## Common Patterns + +### Handling User Interactions + +```rust +// For clickable elements +let response = ui.add(/* widget */); +if response.clicked() { + // Handle click +} else if response.hovered() { + // Show hover effect (often using show_pointer()) + crate::show_pointer(ui); +} +``` + +### Hover Previews + +```rust +// For elements with hover previews +let resp = ui.add(/* widget */); +resp.on_hover_ui_at_pointer(|ui| { + ui.set_max_width(300.0); + ui.add(ProfilePreview::new(profile, img_cache)); +}); +``` + +### Context Menus + +```rust +// For elements with context menus +let resp = ui.add(/* widget */); +resp.context_menu(|ui| { + if ui.button("Menu Option").clicked() { + // Handle selection + ui.close_menu(); + } +}); +``` + +## Contributing Guidelines + +When contributing to `notedeck_ui`: + +1. **Widget Consistency**: Follow established patterns for new widgets +2. **Option Naming**: Keep option names consistent (has_X/set_X pairs) +3. **Performance**: Add profiling annotations to expensive operations +4. **Error Handling**: Propagate errors up rather than handling them directly in UI components +5. **Documentation**: Document public APIs and components with examples +6. **Theme Support**: Ensure components work in both light and dark mode diff --git a/crates/notedeck_ui/docs/components.md b/crates/notedeck_ui/docs/components.md @@ -0,0 +1,321 @@ +# NoteDeck UI Component Guide + +This guide provides detailed documentation for the major UI components in the NoteDeck UI library. + +## Table of Contents + +- [Notes](#notes) + - [NoteView](#noteview) + - [NoteContents](#notecontents) + - [NoteOptions](#noteoptions) +- [Profiles](#profiles) + - [ProfilePic](#profilepic) + - [ProfilePreview](#profilepreview) +- [Mentions](#mentions) +- [Media Handling](#media-handling) + - [Images](#images) + - [GIF Animation](#gif-animation) +- [Widgets & Utilities](#widgets--utilities) + +## Notes + +### NoteView + +The `NoteView` component is the main container for displaying Nostr notes, handling the layout of profile pictures, author information, content, and interactive elements. + +#### Usage + +```rust +let mut note_view = NoteView::new( + note_context, // NoteContext with DB, cache, etc. + current_acc, // Current user account (Option<KeypairUnowned>) + &note, // Reference to Note + options // NoteOptions (display configuration) +); + +// Configure display options +note_view + .actionbar(true) // Show/hide action bar + .small_pfp(false) // Use small profile picture + .medium_pfp(true) // Use medium profile picture + .wide(false) // Use wide layout + .frame(true) // Display with a frame + .note_previews(true) // Enable embedded note previews + .selectable_text(true); // Allow text selection + +// Render the note view +let note_response = note_view.show(ui); + +// Handle user actions +if let Some(action) = note_response.action { + match action { + NoteAction::Note(note_id) => { /* Note was clicked */ }, + NoteAction::Profile(pubkey) => { /* Profile was clicked */ }, + NoteAction::Reply(note_id) => { /* User clicked reply */ }, + NoteAction::Quote(note_id) => { /* User clicked quote */ }, + NoteAction::Zap(zap_action) => { /* User initiated zap */ }, + NoteAction::Hashtag(tag) => { /* Hashtag was clicked */ }, + NoteAction::Context(ctx_selection) => { /* Context menu option selected */ }, + } +} +``` + +#### Layouts + +`NoteView` supports two main layouts: + +1. **Standard Layout** - Default compact display +2. **Wide Layout** - More spacious layout with profile picture on the left + +Use the `.wide(true)` option to enable the wide layout. + +#### Preview Style + +For displaying note previews (e.g., when a note is referenced in another note), use the preview style: + +```rust +let mut note_view = NoteView::new(note_context, current_acc, &note, options) + .preview_style(); // Applies preset options for preview display +``` + +### NoteContents + +`NoteContents` handles rendering the actual content of a note, including text, mentions, hashtags, URLs, and embedded media. + +```rust +let mut contents = NoteContents::new( + note_context, + current_acc, + transaction, + note, + note_options +); + +ui.add(&mut contents); + +// Check for content interactions +if let Some(action) = contents.action() { + // Handle content action (e.g., clicked mention/hashtag) +} +``` + +### NoteOptions + +`NoteOptions` is a bitflag-based configuration system for controlling how notes are displayed: + +```rust +// Create with default options +let mut options = NoteOptions::default(); + +// Or customize from scratch +let mut options = NoteOptions::new(is_universe_timeline); + +// Configure options +options.set_actionbar(true); // Show action buttons +options.set_small_pfp(true); // Use small profile picture +options.set_medium_pfp(false); // Don't use medium profile picture +options.set_note_previews(true); // Enable note previews +options.set_wide(false); // Use compact layout +options.set_selectable_text(true); // Allow text selection +options.set_textmode(false); // Don't use text-only mode +options.set_options_button(true); // Show options button +options.set_hide_media(false); // Show media content +options.set_scramble_text(false); // Don't scramble text +options.set_is_preview(false); // This is not a preview +``` + +## Profiles + +### ProfilePic + +`ProfilePic` displays a circular profile picture with optional border and configurable size. + +```rust +// Basic usage +ui.add(ProfilePic::new(img_cache, profile_url)); + +// Customized +ui.add( + ProfilePic::new(img_cache, profile_url) + .size(48.0) + .border(Stroke::new(2.0, Color32::WHITE)) +); + +// From profile record +if let Some(profile_pic) = ProfilePic::from_profile(img_cache, profile) { + ui.add(profile_pic); +} +``` + +Standard sizes: +- `ProfilePic::default_size()` - 38px +- `ProfilePic::medium_size()` - 32px +- `ProfilePic::small_size()` - 24px + +### ProfilePreview + +`ProfilePreview` shows a detailed profile card with banner, profile picture, display name, username, and about text. + +```rust +// Full preview +ui.add(ProfilePreview::new(profile, img_cache)); + +// Simple preview +ui.add(SimpleProfilePreview::new( + Some(profile), // Option<&ProfileRecord> + img_cache, + is_nsec // Whether this is a full keypair +)); +``` + +## Mentions + +The `Mention` component renders a clickable @username reference with hover preview. + +```rust +let mention_response = Mention::new(ndb, img_cache, txn, pubkey) + .size(16.0) // Text size + .selectable(false) // Disable text selection + .show(ui); + +// Handle mention click +if let Some(action) = mention_response.inner { + // Usually NoteAction::Profile +} +``` + +## Media Handling + +### Images + +Images are managed through the `render_images` function, which handles loading, caching, and displaying images: + +```rust +render_images( + ui, + img_cache, + url, + ImageType::Content, // Or ImageType::Profile(size) + MediaCacheType::Image, + |ui| { + // Show while loading + ui.spinner(); + }, + |ui, error| { + // Show on error + ui.label(format!("Error: {}", error)); + }, + |ui, url, img, gifs| { + // Show successful image + let texture = handle_repaint(ui, retrieve_latest_texture(url, gifs, img)); + ui.image(texture); + } +); +``` + +For profile images, use `ImageType::Profile(size)` to automatically crop, resize, and round the image. + +### GIF Animation + +GIFs are supported through the animation system. The process for displaying GIFs is: + +1. Load and decode GIF in background thread +2. Send frames to UI thread through channels +3. Render frames with timing control + +```rust +// Display a GIF +render_images( + ui, + img_cache, + gif_url, + ImageType::Content, + MediaCacheType::Gif, + /* callbacks as above */ +); + +// Get the current frame texture +let texture = handle_repaint( + ui, + retrieve_latest_texture(url, gifs, renderable_media) +); +``` + +## Widgets & Utilities + +### Username + +Displays a user's name with options for abbreviation and color: + +```rust +ui.add( + Username::new(profile, pubkey) + .pk_colored(true) // Color based on pubkey + .abbreviated(16) // Max length before abbreviation +); +``` + +### Animations + +Use animation helpers for interactive elements: + +```rust +// Basic hover animation +let (rect, size, response) = hover_expand( + ui, + id, // Unique ID for the animation + base_size, // Base size + expand_size, // Amount to expand by + anim_speed // Animation speed +); + +// Small hover expand (common pattern) +let (rect, size, response) = hover_expand_small(ui, id); + +// Advanced helper +let helper = AnimationHelper::new(ui, "animation_name", max_size); +let current_size = helper.scale_1d_pos(min_size); +``` + +### Pulsing Effects + +For elements that need attention: + +```rust +// Create pulsing image +let pulsing_image = ImagePulseTint::new( + &ctx, // EGUI Context + id, // Animation ID + image, // Base image + &[255, 183, 87], // Tint color + alpha_min, // Minimum alpha + alpha_max // Maximum alpha +) +.with_speed(0.35) // Animation speed +.animate(); // Apply animation + +ui.add(pulsing_image); +``` + +### Context Menus + +Create menus for additional actions: + +```rust +// Add context menu to any response +response.context_menu(|ui| { + if ui.button("Copy Link").clicked() { + ui.ctx().copy_text(url.to_owned()); + ui.close_menu(); + } +}); +``` + +The `NoteContextButton` component provides a standard context menu for notes: + +```rust +let resp = ui.add(NoteContextButton::new(note_key)); +if let Some(action) = NoteContextButton::menu(ui, resp) { + // Handle context action +} +```