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:
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,
+ ¬e,
+ 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, ¬e, 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, ¬e, 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>)
+ ¬e, // 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, ¬e, 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
+}
+```