Files
notedeck/crates/notedeck_columns/src/ui/note/context.rs
William Casarin ec755493d9 Introducing Damus Notedeck: a nostr browser
This splits notedeck into:

- notedeck
- notedeck_chrome
- notedeck_columns

The `notedeck` crate is the library that `notedeck_chrome` and
`notedeck_columns`, use. It contains common functionality related to
notedeck apps such as the NoteCache, ImageCache, etc.

The `notedeck_chrome` crate is the binary and ui chrome. It is
responsible for managing themes, user accounts, signing, data paths,
nostrdb, image caches etc. It will eventually have its own ui which has
yet to be determined.  For now it just manages the browser data, which
is passed to apps via a new struct called `AppContext`.

`notedeck_columns` is our columns app, with less responsibility now that
more things are handled by `notedeck_chrome`

There is still much work left to do before this is a proper browser:

- process isolation
- sandboxing
- etc

This is the beginning of a new era! We're just getting started.

Signed-off-by: William Casarin <jb55@jb55.com>
2024-12-12 20:08:55 -08:00

195 lines
5.8 KiB
Rust

use egui::{Rect, Vec2};
use enostr::{NoteId, Pubkey};
use nostrdb::{Note, NoteKey};
use tracing::error;
#[derive(Clone)]
#[allow(clippy::enum_variant_names)]
pub enum NoteContextSelection {
CopyText,
CopyPubkey,
CopyNoteId,
CopyNoteJSON,
}
impl NoteContextSelection {
pub fn process(&self, ui: &mut egui::Ui, note: &Note<'_>) {
match self {
NoteContextSelection::CopyText => {
ui.output_mut(|w| {
w.copied_text = note.content().to_string();
});
}
NoteContextSelection::CopyPubkey => {
ui.output_mut(|w| {
if let Some(bech) = Pubkey::new(*note.pubkey()).to_bech() {
w.copied_text = bech;
}
});
}
NoteContextSelection::CopyNoteId => {
ui.output_mut(|w| {
if let Some(bech) = NoteId::new(*note.id()).to_bech() {
w.copied_text = bech;
}
});
}
NoteContextSelection::CopyNoteJSON => {
ui.output_mut(|w| match note.json() {
Ok(json) => w.copied_text = json,
Err(err) => error!("error copying note json: {err}"),
});
}
}
}
}
pub struct NoteContextButton {
put_at: Option<Rect>,
note_key: NoteKey,
}
impl egui::Widget for NoteContextButton {
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
let r = if let Some(r) = self.put_at {
r
} else {
let mut place = ui.available_rect_before_wrap();
let size = Self::max_width();
place.set_width(size);
place.set_height(size);
place
};
Self::show(ui, self.note_key, r)
}
}
impl NoteContextButton {
pub fn new(note_key: NoteKey) -> Self {
let put_at: Option<Rect> = None;
NoteContextButton { note_key, put_at }
}
pub fn place_at(mut self, rect: Rect) -> Self {
self.put_at = Some(rect);
self
}
pub fn max_width() -> f32 {
Self::max_radius() * 3.0 + Self::max_distance_between_circles() * 2.0
}
pub fn size() -> Vec2 {
let width = Self::max_width();
egui::vec2(width, width)
}
fn max_radius() -> f32 {
8.0
}
fn min_radius() -> f32 {
Self::max_radius() / Self::expansion_multiple()
}
fn max_distance_between_circles() -> f32 {
2.0
}
fn expansion_multiple() -> f32 {
2.0
}
fn min_distance_between_circles() -> f32 {
Self::max_distance_between_circles() / Self::expansion_multiple()
}
pub fn show(ui: &mut egui::Ui, note_key: NoteKey, put_at: Rect) -> egui::Response {
#[cfg(feature = "profiling")]
puffin::profile_function!();
let id = ui.id().with(("more_options_anim", note_key));
let min_radius = Self::min_radius();
let anim_speed = 0.05;
let response = ui.interact(put_at, id, egui::Sense::click());
let hovered = response.hovered();
let animation_progress = ui.ctx().animate_bool_with_time(id, hovered, anim_speed);
if hovered {
ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
}
let min_distance = Self::min_distance_between_circles();
let cur_distance = min_distance
+ (Self::max_distance_between_circles() - min_distance) * animation_progress;
let cur_radius = min_radius + (Self::max_radius() - min_radius) * animation_progress;
let center = put_at.center();
let left_circle_center = center - egui::vec2(cur_distance + cur_radius, 0.0);
let right_circle_center = center + egui::vec2(cur_distance + cur_radius, 0.0);
let translated_radius = (cur_radius - 1.0) / 2.0;
// This works in both themes
let color = ui.style().visuals.noninteractive().fg_stroke.color;
// Draw circles
let painter = ui.painter_at(put_at);
painter.circle_filled(left_circle_center, translated_radius, color);
painter.circle_filled(center, translated_radius, color);
painter.circle_filled(right_circle_center, translated_radius, color);
response
}
pub fn menu(
ui: &mut egui::Ui,
button_response: egui::Response,
) -> Option<NoteContextSelection> {
#[cfg(feature = "profiling")]
puffin::profile_function!();
let mut context_selection: Option<NoteContextSelection> = None;
stationary_arbitrary_menu_button(ui, button_response, |ui| {
ui.set_max_width(200.0);
if ui.button("Copy text").clicked() {
context_selection = Some(NoteContextSelection::CopyText);
ui.close_menu();
}
if ui.button("Copy user public key").clicked() {
context_selection = Some(NoteContextSelection::CopyPubkey);
ui.close_menu();
}
if ui.button("Copy note id").clicked() {
context_selection = Some(NoteContextSelection::CopyNoteId);
ui.close_menu();
}
if ui.button("Copy note json").clicked() {
context_selection = Some(NoteContextSelection::CopyNoteJSON);
ui.close_menu();
}
});
context_selection
}
}
fn stationary_arbitrary_menu_button<R>(
ui: &mut egui::Ui,
button_response: egui::Response,
add_contents: impl FnOnce(&mut egui::Ui) -> R,
) -> egui::InnerResponse<Option<R>> {
let bar_id = ui.id();
let mut bar_state = egui::menu::BarState::load(ui.ctx(), bar_id);
let inner = bar_state.bar_menu(&button_response, add_contents);
bar_state.store(ui.ctx(), bar_id);
egui::InnerResponse::new(inner.map(|r| r.inner), button_response)
}