ui: move note and profile rendering to notedeck_ui
We want to render notes in other apps like dave, so lets move our note rendering to notedeck_ui. We rework NoteAction so it doesn't have anything specific to notedeck_columns Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
@@ -5,7 +5,7 @@ use nostrdb::{Ndb, Transaction};
|
||||
use notedeck::{Accounts, Images};
|
||||
use notedeck_ui::colors::PINK;
|
||||
|
||||
use super::profile::preview::SimpleProfilePreview;
|
||||
use notedeck_ui::profile::preview::SimpleProfilePreview;
|
||||
|
||||
pub struct AccountsView<'a> {
|
||||
ndb: &'a Ndb,
|
||||
|
||||
@@ -13,14 +13,15 @@ use crate::{
|
||||
login_manager::AcquireKeyState,
|
||||
route::Route,
|
||||
timeline::{kind::ListKind, PubkeySource, TimelineKind},
|
||||
ui::anim::ICON_EXPANSION_MULTIPLE,
|
||||
Damus,
|
||||
};
|
||||
|
||||
use notedeck::{AppContext, Images, NotedeckTextStyle, UserAccount};
|
||||
use notedeck_ui::anim::ICON_EXPANSION_MULTIPLE;
|
||||
use tokenator::{ParseError, TokenParser, TokenSerializable, TokenWriter};
|
||||
|
||||
use super::{anim::AnimationHelper, padding, widgets::styled_button, ProfilePreview};
|
||||
use crate::ui::widgets::styled_button;
|
||||
use notedeck_ui::{anim::AnimationHelper, padding, ProfilePreview};
|
||||
|
||||
pub enum AddColumnResponse {
|
||||
Timeline(TimelineKind),
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
use egui::{Pos2, Rect, Response, Sense};
|
||||
|
||||
pub fn hover_expand(
|
||||
ui: &mut egui::Ui,
|
||||
id: egui::Id,
|
||||
size: f32,
|
||||
expand_size: f32,
|
||||
anim_speed: f32,
|
||||
) -> (egui::Rect, f32, egui::Response) {
|
||||
// Allocate space for the profile picture with a fixed size
|
||||
let default_size = size + expand_size;
|
||||
let (rect, response) =
|
||||
ui.allocate_exact_size(egui::vec2(default_size, default_size), egui::Sense::click());
|
||||
|
||||
let val = ui
|
||||
.ctx()
|
||||
.animate_bool_with_time(id, response.hovered(), anim_speed);
|
||||
|
||||
let size = size + val * expand_size;
|
||||
(rect, size, response)
|
||||
}
|
||||
|
||||
pub fn hover_expand_small(ui: &mut egui::Ui, id: egui::Id) -> (egui::Rect, f32, egui::Response) {
|
||||
let size = 10.0;
|
||||
let expand_size = 5.0;
|
||||
let anim_speed = 0.05;
|
||||
|
||||
hover_expand(ui, id, size, expand_size, anim_speed)
|
||||
}
|
||||
|
||||
pub static ICON_EXPANSION_MULTIPLE: f32 = 1.2;
|
||||
pub static ANIM_SPEED: f32 = 0.05;
|
||||
pub struct AnimationHelper {
|
||||
rect: Rect,
|
||||
center: Pos2,
|
||||
response: Response,
|
||||
animation_progress: f32,
|
||||
expansion_multiple: f32,
|
||||
}
|
||||
|
||||
impl AnimationHelper {
|
||||
pub fn new(
|
||||
ui: &mut egui::Ui,
|
||||
animation_name: impl std::hash::Hash,
|
||||
max_size: egui::Vec2,
|
||||
) -> Self {
|
||||
let id = ui.id().with(animation_name);
|
||||
let (rect, response) = ui.allocate_exact_size(max_size, Sense::click());
|
||||
|
||||
let animation_progress =
|
||||
ui.ctx()
|
||||
.animate_bool_with_time(id, response.hovered(), ANIM_SPEED);
|
||||
|
||||
Self {
|
||||
rect,
|
||||
center: rect.center(),
|
||||
response,
|
||||
animation_progress,
|
||||
expansion_multiple: ICON_EXPANSION_MULTIPLE,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn no_animation(ui: &mut egui::Ui, size: egui::Vec2) -> Self {
|
||||
let (rect, response) = ui.allocate_exact_size(size, Sense::hover());
|
||||
|
||||
Self {
|
||||
rect,
|
||||
center: rect.center(),
|
||||
response,
|
||||
animation_progress: 0.0,
|
||||
expansion_multiple: ICON_EXPANSION_MULTIPLE,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_from_rect(
|
||||
ui: &mut egui::Ui,
|
||||
animation_name: impl std::hash::Hash,
|
||||
animation_rect: egui::Rect,
|
||||
) -> Self {
|
||||
let id = ui.id().with(animation_name);
|
||||
let response = ui.allocate_rect(animation_rect, Sense::click());
|
||||
|
||||
let animation_progress =
|
||||
ui.ctx()
|
||||
.animate_bool_with_time(id, response.hovered(), ANIM_SPEED);
|
||||
|
||||
Self {
|
||||
rect: animation_rect,
|
||||
center: animation_rect.center(),
|
||||
response,
|
||||
animation_progress,
|
||||
expansion_multiple: ICON_EXPANSION_MULTIPLE,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scale_1d_pos(&self, min_object_size: f32) -> f32 {
|
||||
let max_object_size = min_object_size * self.expansion_multiple;
|
||||
|
||||
if self.response.is_pointer_button_down_on() {
|
||||
min_object_size
|
||||
} else {
|
||||
min_object_size + ((max_object_size - min_object_size) * self.animation_progress)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scale_radius(&self, min_diameter: f32) -> f32 {
|
||||
self.scale_1d_pos((min_diameter - 1.0) / 2.0)
|
||||
}
|
||||
|
||||
pub fn get_animation_rect(&self) -> egui::Rect {
|
||||
self.rect
|
||||
}
|
||||
|
||||
pub fn center(&self) -> Pos2 {
|
||||
self.rect.center()
|
||||
}
|
||||
|
||||
pub fn take_animation_response(self) -> egui::Response {
|
||||
self.response
|
||||
}
|
||||
|
||||
// Scale a minimum position from center to the current animation position
|
||||
pub fn scale_from_center(&self, x_min: f32, y_min: f32) -> Pos2 {
|
||||
Pos2::new(
|
||||
self.center.x + self.scale_1d_pos(x_min),
|
||||
self.center.y + self.scale_1d_pos(y_min),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn scale_pos_from_center(&self, min_pos: Pos2) -> Pos2 {
|
||||
self.scale_from_center(min_pos.x, min_pos.y)
|
||||
}
|
||||
|
||||
/// New method for min/max scaling when needed
|
||||
pub fn scale_1d_pos_min_max(&self, min_object_size: f32, max_object_size: f32) -> f32 {
|
||||
min_object_size + ((max_object_size - min_object_size) * self.animation_progress)
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,7 @@ use crate::{
|
||||
column::Columns,
|
||||
route::Route,
|
||||
timeline::{ColumnTitle, TimelineKind},
|
||||
ui::{
|
||||
self,
|
||||
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
|
||||
},
|
||||
ui::{self},
|
||||
};
|
||||
|
||||
use egui::Margin;
|
||||
@@ -16,6 +13,10 @@ use egui::{RichText, Stroke, UiBuilder};
|
||||
use enostr::Pubkey;
|
||||
use nostrdb::{Ndb, Transaction};
|
||||
use notedeck::{Images, NotedeckTextStyle};
|
||||
use notedeck_ui::{
|
||||
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
|
||||
ProfilePic,
|
||||
};
|
||||
|
||||
pub struct NavTitle<'a> {
|
||||
ndb: &'a Ndb,
|
||||
@@ -43,7 +44,7 @@ impl<'a> NavTitle<'a> {
|
||||
}
|
||||
|
||||
pub fn show(&mut self, ui: &mut egui::Ui) -> Option<RenderNavAction> {
|
||||
ui::padding(8.0, ui, |ui| {
|
||||
notedeck_ui::padding(8.0, ui, |ui| {
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(48.0);
|
||||
|
||||
@@ -72,7 +73,7 @@ impl<'a> NavTitle<'a> {
|
||||
|
||||
if let Some(back_resp) = &back_button_resp {
|
||||
if back_resp.hovered() || back_resp.clicked() {
|
||||
ui::show_pointer(ui);
|
||||
notedeck_ui::show_pointer(ui);
|
||||
}
|
||||
} else {
|
||||
// add some space where chevron would have been. this makes the ui
|
||||
@@ -220,7 +221,7 @@ impl<'a> NavTitle<'a> {
|
||||
}
|
||||
});
|
||||
} else if move_resp.hovered() {
|
||||
ui::show_pointer(ui);
|
||||
notedeck_ui::show_pointer(ui);
|
||||
}
|
||||
|
||||
ui.data(|d| d.get_temp(cur_id)).and_then(|val| {
|
||||
@@ -388,14 +389,12 @@ impl<'a> NavTitle<'a> {
|
||||
txn: &'txn Transaction,
|
||||
pubkey: &[u8; 32],
|
||||
pfp_size: f32,
|
||||
) -> Option<ui::ProfilePic<'me, 'txn>> {
|
||||
) -> Option<ProfilePic<'me, 'txn>> {
|
||||
self.ndb
|
||||
.get_profile_by_pubkey(txn, pubkey)
|
||||
.as_ref()
|
||||
.ok()
|
||||
.and_then(move |p| {
|
||||
Some(ui::ProfilePic::from_profile(self.img_cache, p)?.size(pfp_size))
|
||||
})
|
||||
.and_then(move |p| Some(ProfilePic::from_profile(self.img_cache, p)?.size(pfp_size)))
|
||||
}
|
||||
|
||||
fn timeline_pfp(&mut self, ui: &mut egui::Ui, id: &TimelineKind, pfp_size: f32) {
|
||||
@@ -407,9 +406,7 @@ impl<'a> NavTitle<'a> {
|
||||
{
|
||||
ui.add(pfp);
|
||||
} else {
|
||||
ui.add(
|
||||
ui::ProfilePic::new(self.img_cache, ui::ProfilePic::no_pfp_url()).size(pfp_size),
|
||||
);
|
||||
ui.add(ProfilePic::new(self.img_cache, notedeck::profile::no_pfp_url()).size(pfp_size));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -472,9 +469,7 @@ impl<'a> NavTitle<'a> {
|
||||
if let Some(pfp) = self.pubkey_pfp(&txn, pubkey.bytes(), pfp_size) {
|
||||
ui.add(pfp);
|
||||
} else {
|
||||
ui.add(
|
||||
ui::ProfilePic::new(self.img_cache, ui::ProfilePic::no_pfp_url()).size(pfp_size),
|
||||
);
|
||||
ui.add(ProfilePic::new(self.img_cache, notedeck::profile::no_pfp_url()).size(pfp_size));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use crate::{app_style::deck_icon_font_sized, deck_state::DeckState};
|
||||
use egui::{vec2, Button, Color32, Label, RichText, Stroke, Ui, Widget};
|
||||
use notedeck::{NamedFontFamily, NotedeckTextStyle};
|
||||
use notedeck_ui::colors::PINK;
|
||||
|
||||
use super::{
|
||||
use notedeck_ui::{
|
||||
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
|
||||
colors::PINK,
|
||||
padding,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,10 +2,8 @@ use egui::Widget;
|
||||
|
||||
use crate::deck_state::DeckState;
|
||||
|
||||
use super::{
|
||||
configure_deck::{ConfigureDeckResponse, ConfigureDeckView},
|
||||
padding,
|
||||
};
|
||||
use super::configure_deck::{ConfigureDeckResponse, ConfigureDeckView};
|
||||
use notedeck_ui::padding;
|
||||
|
||||
pub struct EditDeckView<'a> {
|
||||
config_view: ConfigureDeckView<'a>,
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
use crate::ui;
|
||||
use crate::{actionbar::NoteAction, profile::get_display_name, timeline::TimelineKind};
|
||||
use egui::Sense;
|
||||
use enostr::Pubkey;
|
||||
use nostrdb::{Ndb, Transaction};
|
||||
use notedeck::Images;
|
||||
|
||||
pub struct Mention<'a> {
|
||||
ndb: &'a Ndb,
|
||||
img_cache: &'a mut Images,
|
||||
txn: &'a Transaction,
|
||||
pk: &'a [u8; 32],
|
||||
selectable: bool,
|
||||
size: f32,
|
||||
}
|
||||
|
||||
impl<'a> Mention<'a> {
|
||||
pub fn new(
|
||||
ndb: &'a Ndb,
|
||||
img_cache: &'a mut Images,
|
||||
txn: &'a Transaction,
|
||||
pk: &'a [u8; 32],
|
||||
) -> Self {
|
||||
let size = 16.0;
|
||||
let selectable = true;
|
||||
Mention {
|
||||
ndb,
|
||||
img_cache,
|
||||
txn,
|
||||
pk,
|
||||
selectable,
|
||||
size,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selectable(mut self, selectable: bool) -> Self {
|
||||
self.selectable = selectable;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: f32) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn show(self, ui: &mut egui::Ui) -> egui::InnerResponse<Option<NoteAction>> {
|
||||
mention_ui(
|
||||
self.ndb,
|
||||
self.img_cache,
|
||||
self.txn,
|
||||
self.pk,
|
||||
ui,
|
||||
self.size,
|
||||
self.selectable,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl egui::Widget for Mention<'_> {
|
||||
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
|
||||
self.show(ui).response
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[profiling::function]
|
||||
fn mention_ui(
|
||||
ndb: &Ndb,
|
||||
img_cache: &mut Images,
|
||||
txn: &Transaction,
|
||||
pk: &[u8; 32],
|
||||
ui: &mut egui::Ui,
|
||||
size: f32,
|
||||
selectable: bool,
|
||||
) -> egui::InnerResponse<Option<NoteAction>> {
|
||||
let link_color = ui.visuals().hyperlink_color;
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let profile = ndb.get_profile_by_pubkey(txn, pk).ok();
|
||||
|
||||
let name: String = format!("@{}", get_display_name(profile.as_ref()).name());
|
||||
|
||||
let resp = ui.add(
|
||||
egui::Label::new(egui::RichText::new(name).color(link_color).size(size))
|
||||
.sense(Sense::click())
|
||||
.selectable(selectable),
|
||||
);
|
||||
|
||||
let note_action = if resp.clicked() {
|
||||
ui::show_pointer(ui);
|
||||
Some(NoteAction::OpenTimeline(TimelineKind::profile(
|
||||
Pubkey::new(*pk),
|
||||
)))
|
||||
} else if resp.hovered() {
|
||||
ui::show_pointer(ui);
|
||||
None
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(rec) = profile.as_ref() {
|
||||
resp.on_hover_ui_at_pointer(|ui| {
|
||||
ui.set_max_width(300.0);
|
||||
ui.add(ui::ProfilePreview::new(rec, img_cache));
|
||||
});
|
||||
}
|
||||
|
||||
note_action
|
||||
})
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
pub mod account_login_view;
|
||||
pub mod accounts;
|
||||
pub mod add_column;
|
||||
pub mod anim;
|
||||
pub mod column;
|
||||
pub mod configure_deck;
|
||||
pub mod edit_deck;
|
||||
pub mod images;
|
||||
pub mod mention;
|
||||
pub mod note;
|
||||
pub mod preview;
|
||||
pub mod profile;
|
||||
@@ -17,56 +15,14 @@ pub mod side_panel;
|
||||
pub mod support;
|
||||
pub mod thread;
|
||||
pub mod timeline;
|
||||
pub mod username;
|
||||
pub mod wallet;
|
||||
pub mod widgets;
|
||||
|
||||
pub use accounts::AccountsView;
|
||||
pub use mention::Mention;
|
||||
pub use note::{NoteResponse, NoteView, PostReplyView, PostView};
|
||||
pub use notedeck_ui::ProfilePic;
|
||||
pub use note::{PostReplyView, PostView};
|
||||
pub use preview::{Preview, PreviewApp, PreviewConfig};
|
||||
pub use profile::ProfilePreview;
|
||||
pub use profile::ProfileView;
|
||||
pub use relay::RelayView;
|
||||
pub use side_panel::{DesktopSidePanel, SidePanelAction};
|
||||
pub use thread::ThreadView;
|
||||
pub use timeline::TimelineView;
|
||||
pub use username::Username;
|
||||
|
||||
use egui::Margin;
|
||||
|
||||
/// This is kind of like the Widget trait but is meant for larger top-level
|
||||
/// views that are typically stateful.
|
||||
///
|
||||
/// The Widget trait forces us to add mutable
|
||||
/// implementations at the type level, which screws us when generating Previews
|
||||
/// for a Widget. I would have just Widget instead of making this Trait otherwise.
|
||||
///
|
||||
/// There is some precendent for this, it looks like there's a similar trait
|
||||
/// in the egui demo library.
|
||||
pub trait View {
|
||||
fn ui(&mut self, ui: &mut egui::Ui);
|
||||
}
|
||||
|
||||
pub fn padding<R>(
|
||||
amount: impl Into<Margin>,
|
||||
ui: &mut egui::Ui,
|
||||
add_contents: impl FnOnce(&mut egui::Ui) -> R,
|
||||
) -> egui::InnerResponse<R> {
|
||||
egui::Frame::new()
|
||||
.inner_margin(amount)
|
||||
.show(ui, add_contents)
|
||||
}
|
||||
|
||||
pub fn hline(ui: &egui::Ui) {
|
||||
// pixel perfect horizontal line
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
#[allow(deprecated)]
|
||||
let resize_y = ui.painter().round_to_pixel(rect.top()) - 0.5;
|
||||
let stroke = ui.style().visuals.widgets.noninteractive.bg_stroke;
|
||||
ui.painter().hline(rect.x_range(), resize_y, stroke);
|
||||
}
|
||||
|
||||
pub fn show_pointer(ui: &egui::Ui) {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
|
||||
}
|
||||
|
||||
@@ -1,558 +0,0 @@
|
||||
use crate::ui::{
|
||||
self,
|
||||
note::{NoteOptions, NoteResponse},
|
||||
};
|
||||
use crate::{actionbar::NoteAction, timeline::TimelineKind};
|
||||
use egui::{Button, Color32, Hyperlink, Image, Response, RichText, Sense, Window};
|
||||
use enostr::{KeypairUnowned, RelayPool};
|
||||
use nostrdb::{BlockType, Mention, Ndb, Note, NoteKey, Transaction};
|
||||
use notedeck_ui::images::ImageType;
|
||||
use notedeck_ui::{
|
||||
gif::{handle_repaint, retrieve_latest_texture},
|
||||
images::render_images,
|
||||
};
|
||||
use tracing::warn;
|
||||
|
||||
use notedeck::{supported_mime_hosted_at_url, Images, MediaCacheType, NoteCache, Zaps};
|
||||
|
||||
/// Aggregates dependencies to reduce the number of parameters
|
||||
/// passed to inner UI elements, minimizing prop drilling.
|
||||
pub struct NoteContext<'d> {
|
||||
pub ndb: &'d Ndb,
|
||||
pub img_cache: &'d mut Images,
|
||||
pub note_cache: &'d mut NoteCache,
|
||||
pub zaps: &'d mut Zaps,
|
||||
pub pool: &'d mut RelayPool,
|
||||
}
|
||||
|
||||
pub struct NoteContents<'a, 'd> {
|
||||
note_context: &'a mut NoteContext<'d>,
|
||||
cur_acc: &'a Option<KeypairUnowned<'a>>,
|
||||
txn: &'a Transaction,
|
||||
note: &'a Note<'a>,
|
||||
options: NoteOptions,
|
||||
action: Option<NoteAction>,
|
||||
}
|
||||
|
||||
impl<'a, 'd> NoteContents<'a, 'd> {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
note_context: &'a mut NoteContext<'d>,
|
||||
cur_acc: &'a Option<KeypairUnowned<'a>>,
|
||||
txn: &'a Transaction,
|
||||
note: &'a Note,
|
||||
options: ui::note::NoteOptions,
|
||||
) -> Self {
|
||||
NoteContents {
|
||||
note_context,
|
||||
cur_acc,
|
||||
txn,
|
||||
note,
|
||||
options,
|
||||
action: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn action(&self) -> &Option<NoteAction> {
|
||||
&self.action
|
||||
}
|
||||
}
|
||||
|
||||
impl egui::Widget for &mut NoteContents<'_, '_> {
|
||||
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
|
||||
let result = render_note_contents(
|
||||
ui,
|
||||
self.note_context,
|
||||
self.cur_acc,
|
||||
self.txn,
|
||||
self.note,
|
||||
self.options,
|
||||
);
|
||||
self.action = result.action;
|
||||
result.response
|
||||
}
|
||||
}
|
||||
|
||||
/// Render an inline note preview with a border. These are used when
|
||||
/// notes are references within a note
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[profiling::function]
|
||||
pub fn render_note_preview(
|
||||
ui: &mut egui::Ui,
|
||||
note_context: &mut NoteContext,
|
||||
cur_acc: &Option<KeypairUnowned>,
|
||||
txn: &Transaction,
|
||||
id: &[u8; 32],
|
||||
parent: NoteKey,
|
||||
note_options: NoteOptions,
|
||||
) -> NoteResponse {
|
||||
let note = if let Ok(note) = note_context.ndb.get_note_by_id(txn, id) {
|
||||
// TODO: support other preview kinds
|
||||
if note.kind() == 1 {
|
||||
note
|
||||
} else {
|
||||
return NoteResponse::new(ui.colored_label(
|
||||
Color32::RED,
|
||||
format!("TODO: can't preview kind {}", note.kind()),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return NoteResponse::new(ui.colored_label(Color32::RED, "TODO: COULD NOT LOAD"));
|
||||
/*
|
||||
return ui
|
||||
.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.colored_label(link_color, "@");
|
||||
ui.colored_label(link_color, &id_str[4..16]);
|
||||
})
|
||||
.response;
|
||||
*/
|
||||
};
|
||||
|
||||
egui::Frame::new()
|
||||
.fill(ui.visuals().noninteractive().weak_bg_fill)
|
||||
.inner_margin(egui::Margin::same(8))
|
||||
.outer_margin(egui::Margin::symmetric(0, 8))
|
||||
.corner_radius(egui::CornerRadius::same(10))
|
||||
.stroke(egui::Stroke::new(
|
||||
1.0,
|
||||
ui.visuals().noninteractive().bg_stroke.color,
|
||||
))
|
||||
.show(ui, |ui| {
|
||||
ui::NoteView::new(note_context, cur_acc, ¬e, note_options)
|
||||
.actionbar(false)
|
||||
.small_pfp(true)
|
||||
.wide(true)
|
||||
.note_previews(false)
|
||||
.options_button(true)
|
||||
.parent(parent)
|
||||
.is_preview(true)
|
||||
.show(ui)
|
||||
})
|
||||
.inner
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[profiling::function]
|
||||
fn render_note_contents(
|
||||
ui: &mut egui::Ui,
|
||||
note_context: &mut NoteContext,
|
||||
cur_acc: &Option<KeypairUnowned>,
|
||||
txn: &Transaction,
|
||||
note: &Note,
|
||||
options: NoteOptions,
|
||||
) -> NoteResponse {
|
||||
let note_key = note.key().expect("todo: implement non-db notes");
|
||||
let selectable = options.has_selectable_text();
|
||||
let mut images: Vec<(String, MediaCacheType)> = vec![];
|
||||
let mut note_action: Option<NoteAction> = None;
|
||||
let mut inline_note: Option<(&[u8; 32], &str)> = None;
|
||||
let hide_media = options.has_hide_media();
|
||||
let link_color = ui.visuals().hyperlink_color;
|
||||
|
||||
if !options.has_is_preview() {
|
||||
// need this for the rect to take the full width of the column
|
||||
let _ = ui.allocate_at_least(egui::vec2(ui.available_width(), 0.0), egui::Sense::click());
|
||||
}
|
||||
|
||||
let response = ui.horizontal_wrapped(|ui| {
|
||||
let blocks = if let Ok(blocks) = note_context.ndb.get_blocks_by_key(txn, note_key) {
|
||||
blocks
|
||||
} else {
|
||||
warn!("missing note content blocks? '{}'", note.content());
|
||||
ui.weak(note.content());
|
||||
return;
|
||||
};
|
||||
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
|
||||
for block in blocks.iter(note) {
|
||||
match block.blocktype() {
|
||||
BlockType::MentionBech32 => match block.as_mention().unwrap() {
|
||||
Mention::Profile(profile) => {
|
||||
let act = ui::Mention::new(
|
||||
note_context.ndb,
|
||||
note_context.img_cache,
|
||||
txn,
|
||||
profile.pubkey(),
|
||||
)
|
||||
.show(ui)
|
||||
.inner;
|
||||
if act.is_some() {
|
||||
note_action = act;
|
||||
}
|
||||
}
|
||||
|
||||
Mention::Pubkey(npub) => {
|
||||
let act = ui::Mention::new(
|
||||
note_context.ndb,
|
||||
note_context.img_cache,
|
||||
txn,
|
||||
npub.pubkey(),
|
||||
)
|
||||
.show(ui)
|
||||
.inner;
|
||||
if act.is_some() {
|
||||
note_action = act;
|
||||
}
|
||||
}
|
||||
|
||||
Mention::Note(note) if options.has_note_previews() => {
|
||||
inline_note = Some((note.id(), block.as_str()));
|
||||
}
|
||||
|
||||
Mention::Event(note) if options.has_note_previews() => {
|
||||
inline_note = Some((note.id(), block.as_str()));
|
||||
}
|
||||
|
||||
_ => {
|
||||
ui.colored_label(link_color, format!("@{}", &block.as_str()[4..16]));
|
||||
}
|
||||
},
|
||||
|
||||
BlockType::Hashtag => {
|
||||
let resp = ui.colored_label(link_color, format!("#{}", block.as_str()));
|
||||
|
||||
if resp.clicked() {
|
||||
note_action = Some(NoteAction::OpenTimeline(TimelineKind::Hashtag(
|
||||
block.as_str().to_string(),
|
||||
)));
|
||||
} else if resp.hovered() {
|
||||
ui::show_pointer(ui);
|
||||
}
|
||||
}
|
||||
|
||||
BlockType::Url => {
|
||||
let mut found_supported = || -> bool {
|
||||
let url = block.as_str();
|
||||
if let Some(cache_type) =
|
||||
supported_mime_hosted_at_url(&mut note_context.img_cache.urls, url)
|
||||
{
|
||||
images.push((url.to_string(), cache_type));
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
if hide_media || !found_supported() {
|
||||
ui.add(Hyperlink::from_label_and_url(
|
||||
RichText::new(block.as_str()).color(link_color),
|
||||
block.as_str(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
BlockType::Text => {
|
||||
if options.has_scramble_text() {
|
||||
ui.add(egui::Label::new(rot13(block.as_str())).selectable(selectable));
|
||||
} else {
|
||||
ui.add(egui::Label::new(block.as_str()).selectable(selectable));
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
ui.colored_label(link_color, block.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let preview_note_action = if let Some((id, _block_str)) = inline_note {
|
||||
render_note_preview(ui, note_context, cur_acc, txn, id, note_key, options).action
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if !images.is_empty() && !options.has_textmode() {
|
||||
ui.add_space(2.0);
|
||||
let carousel_id = egui::Id::new(("carousel", note.key().expect("expected tx note")));
|
||||
image_carousel(ui, note_context.img_cache, images, carousel_id);
|
||||
ui.add_space(2.0);
|
||||
}
|
||||
|
||||
let note_action = preview_note_action.or(note_action);
|
||||
|
||||
NoteResponse::new(response.response).with_action(note_action)
|
||||
}
|
||||
|
||||
fn rot13(input: &str) -> String {
|
||||
input
|
||||
.chars()
|
||||
.map(|c| {
|
||||
if c.is_ascii_lowercase() {
|
||||
// Rotate lowercase letters
|
||||
(((c as u8 - b'a' + 13) % 26) + b'a') as char
|
||||
} else if c.is_ascii_uppercase() {
|
||||
// Rotate uppercase letters
|
||||
(((c as u8 - b'A' + 13) % 26) + b'A') as char
|
||||
} else {
|
||||
// Leave other characters unchanged
|
||||
c
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn image_carousel(
|
||||
ui: &mut egui::Ui,
|
||||
img_cache: &mut Images,
|
||||
images: Vec<(String, MediaCacheType)>,
|
||||
carousel_id: egui::Id,
|
||||
) {
|
||||
// let's make sure everything is within our area
|
||||
|
||||
let height = 360.0;
|
||||
let width = ui.available_size().x;
|
||||
let spinsz = if height > width { width } else { height };
|
||||
|
||||
let show_popup = ui.ctx().memory(|mem| {
|
||||
mem.data
|
||||
.get_temp(carousel_id.with("show_popup"))
|
||||
.unwrap_or(false)
|
||||
});
|
||||
|
||||
let current_image = show_popup.then(|| {
|
||||
ui.ctx().memory(|mem| {
|
||||
mem.data
|
||||
.get_temp::<(String, MediaCacheType)>(carousel_id.with("current_image"))
|
||||
.unwrap_or_else(|| (images[0].0.clone(), images[0].1.clone()))
|
||||
})
|
||||
});
|
||||
|
||||
ui.add_sized([width, height], |ui: &mut egui::Ui| {
|
||||
egui::ScrollArea::horizontal()
|
||||
.id_salt(carousel_id)
|
||||
.show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
for (image, cache_type) in images {
|
||||
render_images(
|
||||
ui,
|
||||
img_cache,
|
||||
&image,
|
||||
ImageType::Content,
|
||||
cache_type.clone(),
|
||||
|ui| {
|
||||
ui.allocate_space(egui::vec2(spinsz, spinsz));
|
||||
},
|
||||
|ui, _| {
|
||||
ui.allocate_space(egui::vec2(spinsz, spinsz));
|
||||
},
|
||||
|ui, url, renderable_media, gifs| {
|
||||
let texture = handle_repaint(
|
||||
ui,
|
||||
retrieve_latest_texture(&image, gifs, renderable_media),
|
||||
);
|
||||
let img_resp = ui.add(
|
||||
Button::image(
|
||||
Image::new(texture)
|
||||
.max_height(height)
|
||||
.corner_radius(5.0)
|
||||
.fit_to_original_size(1.0),
|
||||
)
|
||||
.frame(false),
|
||||
);
|
||||
|
||||
if img_resp.clicked() {
|
||||
ui.ctx().memory_mut(|mem| {
|
||||
mem.data.insert_temp(carousel_id.with("show_popup"), true);
|
||||
mem.data.insert_temp(
|
||||
carousel_id.with("current_image"),
|
||||
(image.clone(), cache_type.clone()),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
copy_link(url, img_resp);
|
||||
},
|
||||
);
|
||||
}
|
||||
})
|
||||
.response
|
||||
})
|
||||
.inner
|
||||
});
|
||||
|
||||
if show_popup {
|
||||
let current_image = current_image
|
||||
.as_ref()
|
||||
.expect("the image was actually clicked");
|
||||
let image = current_image.clone().0;
|
||||
let cache_type = current_image.clone().1;
|
||||
|
||||
Window::new("image_popup")
|
||||
.title_bar(false)
|
||||
.fixed_size(ui.ctx().screen_rect().size())
|
||||
.fixed_pos(ui.ctx().screen_rect().min)
|
||||
.frame(egui::Frame::NONE)
|
||||
.show(ui.ctx(), |ui| {
|
||||
let screen_rect = ui.ctx().screen_rect();
|
||||
|
||||
// escape
|
||||
if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||
ui.ctx().memory_mut(|mem| {
|
||||
mem.data.insert_temp(carousel_id.with("show_popup"), false);
|
||||
});
|
||||
}
|
||||
|
||||
// background
|
||||
ui.painter()
|
||||
.rect_filled(screen_rect, 0.0, Color32::from_black_alpha(230));
|
||||
|
||||
// zoom init
|
||||
let zoom_id = carousel_id.with("zoom_level");
|
||||
let mut zoom = ui
|
||||
.ctx()
|
||||
.memory(|mem| mem.data.get_temp(zoom_id).unwrap_or(1.0_f32));
|
||||
|
||||
// pan init
|
||||
let pan_id = carousel_id.with("pan_offset");
|
||||
let mut pan_offset = ui
|
||||
.ctx()
|
||||
.memory(|mem| mem.data.get_temp(pan_id).unwrap_or(egui::Vec2::ZERO));
|
||||
|
||||
// zoom & scroll
|
||||
if ui.input(|i| i.pointer.hover_pos()).is_some() {
|
||||
let scroll_delta = ui.input(|i| i.smooth_scroll_delta);
|
||||
if scroll_delta.y != 0.0 {
|
||||
let zoom_factor = if scroll_delta.y > 0.0 { 1.05 } else { 0.95 };
|
||||
zoom *= zoom_factor;
|
||||
zoom = zoom.clamp(0.1, 5.0);
|
||||
|
||||
if zoom <= 1.0 {
|
||||
pan_offset = egui::Vec2::ZERO;
|
||||
}
|
||||
|
||||
ui.ctx().memory_mut(|mem| {
|
||||
mem.data.insert_temp(zoom_id, zoom);
|
||||
mem.data.insert_temp(pan_id, pan_offset);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ui.centered_and_justified(|ui| {
|
||||
render_images(
|
||||
ui,
|
||||
img_cache,
|
||||
&image,
|
||||
ImageType::Content,
|
||||
cache_type.clone(),
|
||||
|ui| {
|
||||
ui.allocate_space(egui::vec2(spinsz, spinsz));
|
||||
},
|
||||
|ui, _| {
|
||||
ui.allocate_space(egui::vec2(spinsz, spinsz));
|
||||
},
|
||||
|ui, url, renderable_media, gifs| {
|
||||
let texture = handle_repaint(
|
||||
ui,
|
||||
retrieve_latest_texture(&image, gifs, renderable_media),
|
||||
);
|
||||
|
||||
let texture_size = texture.size_vec2();
|
||||
let screen_size = screen_rect.size();
|
||||
let scale = (screen_size.x / texture_size.x)
|
||||
.min(screen_size.y / texture_size.y)
|
||||
.min(1.0);
|
||||
let scaled_size = texture_size * scale * zoom;
|
||||
|
||||
let visible_width = scaled_size.x.min(screen_size.x);
|
||||
let visible_height = scaled_size.y.min(screen_size.y);
|
||||
|
||||
let max_pan_x = ((scaled_size.x - visible_width) / 2.0).max(0.0);
|
||||
let max_pan_y = ((scaled_size.y - visible_height) / 2.0).max(0.0);
|
||||
|
||||
if max_pan_x > 0.0 {
|
||||
pan_offset.x = pan_offset.x.clamp(-max_pan_x, max_pan_x);
|
||||
} else {
|
||||
pan_offset.x = 0.0;
|
||||
}
|
||||
|
||||
if max_pan_y > 0.0 {
|
||||
pan_offset.y = pan_offset.y.clamp(-max_pan_y, max_pan_y);
|
||||
} else {
|
||||
pan_offset.y = 0.0;
|
||||
}
|
||||
|
||||
let (rect, response) = ui.allocate_exact_size(
|
||||
egui::vec2(visible_width, visible_height),
|
||||
egui::Sense::click_and_drag(),
|
||||
);
|
||||
|
||||
let uv_min = egui::pos2(
|
||||
0.5 - (visible_width / scaled_size.x) / 2.0
|
||||
+ pan_offset.x / scaled_size.x,
|
||||
0.5 - (visible_height / scaled_size.y) / 2.0
|
||||
+ pan_offset.y / scaled_size.y,
|
||||
);
|
||||
|
||||
let uv_max = egui::pos2(
|
||||
uv_min.x + visible_width / scaled_size.x,
|
||||
uv_min.y + visible_height / scaled_size.y,
|
||||
);
|
||||
|
||||
let uv = egui::Rect::from_min_max(uv_min, uv_max);
|
||||
|
||||
ui.painter()
|
||||
.image(texture.id(), rect, uv, egui::Color32::WHITE);
|
||||
let img_rect = ui.allocate_rect(rect, Sense::click());
|
||||
|
||||
if img_rect.clicked() {
|
||||
ui.ctx().memory_mut(|mem| {
|
||||
mem.data.insert_temp(carousel_id.with("show_popup"), true);
|
||||
});
|
||||
} else if img_rect.clicked_elsewhere() {
|
||||
ui.ctx().memory_mut(|mem| {
|
||||
mem.data.insert_temp(carousel_id.with("show_popup"), false);
|
||||
});
|
||||
}
|
||||
|
||||
// Handle dragging for pan
|
||||
if response.dragged() {
|
||||
let delta = response.drag_delta();
|
||||
|
||||
pan_offset.x -= delta.x;
|
||||
pan_offset.y -= delta.y;
|
||||
|
||||
if max_pan_x > 0.0 {
|
||||
pan_offset.x = pan_offset.x.clamp(-max_pan_x, max_pan_x);
|
||||
} else {
|
||||
pan_offset.x = 0.0;
|
||||
}
|
||||
|
||||
if max_pan_y > 0.0 {
|
||||
pan_offset.y = pan_offset.y.clamp(-max_pan_y, max_pan_y);
|
||||
} else {
|
||||
pan_offset.y = 0.0;
|
||||
}
|
||||
|
||||
ui.ctx().memory_mut(|mem| {
|
||||
mem.data.insert_temp(pan_id, pan_offset);
|
||||
});
|
||||
}
|
||||
|
||||
// reset zoom on double-click
|
||||
if response.double_clicked() {
|
||||
pan_offset = egui::Vec2::ZERO;
|
||||
zoom = 1.0;
|
||||
ui.ctx().memory_mut(|mem| {
|
||||
mem.data.insert_temp(pan_id, pan_offset);
|
||||
mem.data.insert_temp(zoom_id, zoom);
|
||||
});
|
||||
}
|
||||
|
||||
copy_link(url, response);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_link(url: &str, img_resp: Response) {
|
||||
img_resp.context_menu(|ui| {
|
||||
if ui.button("Copy Link").clicked() {
|
||||
ui.ctx().copy_text(url.to_owned());
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
use egui::{Rect, Vec2};
|
||||
use enostr::{ClientMessage, NoteId, Pubkey, RelayPool};
|
||||
use nostrdb::{Note, NoteKey};
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum BroadcastContext {
|
||||
LocalNetwork,
|
||||
Everywhere,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub enum NoteContextSelection {
|
||||
CopyText,
|
||||
CopyPubkey,
|
||||
CopyNoteId,
|
||||
CopyNoteJSON,
|
||||
Broadcast(BroadcastContext),
|
||||
}
|
||||
|
||||
impl NoteContextSelection {
|
||||
pub fn process(&self, ui: &mut egui::Ui, note: &Note<'_>, pool: &mut RelayPool) {
|
||||
match self {
|
||||
NoteContextSelection::Broadcast(context) => {
|
||||
tracing::info!("Broadcasting note {}", hex::encode(note.id()));
|
||||
match context {
|
||||
BroadcastContext::LocalNetwork => {
|
||||
pool.send_to(&ClientMessage::event(note).unwrap(), "multicast");
|
||||
}
|
||||
|
||||
BroadcastContext::Everywhere => {
|
||||
pool.send(&ClientMessage::event(note).unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
NoteContextSelection::CopyText => {
|
||||
ui.ctx().copy_text(note.content().to_string());
|
||||
}
|
||||
NoteContextSelection::CopyPubkey => {
|
||||
if let Some(bech) = Pubkey::new(*note.pubkey()).to_bech() {
|
||||
ui.ctx().copy_text(bech);
|
||||
}
|
||||
}
|
||||
NoteContextSelection::CopyNoteId => {
|
||||
if let Some(bech) = NoteId::new(*note.id()).to_bech() {
|
||||
ui.ctx().copy_text(bech);
|
||||
}
|
||||
}
|
||||
NoteContextSelection::CopyNoteJSON => match note.json() {
|
||||
Ok(json) => ui.ctx().copy_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 {
|
||||
4.0
|
||||
}
|
||||
|
||||
fn min_radius() -> f32 {
|
||||
2.0
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
pub fn show(ui: &mut egui::Ui, note_key: NoteKey, put_at: Rect) -> egui::Response {
|
||||
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
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
pub fn menu(
|
||||
ui: &mut egui::Ui,
|
||||
button_response: egui::Response,
|
||||
) -> Option<NoteContextSelection> {
|
||||
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();
|
||||
}
|
||||
if ui.button("Broadcast").clicked() {
|
||||
context_selection = Some(NoteContextSelection::Broadcast(
|
||||
BroadcastContext::Everywhere,
|
||||
));
|
||||
ui.close_menu();
|
||||
}
|
||||
if ui.button("Broadcast to local network").clicked() {
|
||||
context_selection = Some(NoteContextSelection::Broadcast(
|
||||
BroadcastContext::LocalNetwork,
|
||||
));
|
||||
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)
|
||||
}
|
||||
@@ -1,765 +1,7 @@
|
||||
pub mod contents;
|
||||
pub mod context;
|
||||
pub mod options;
|
||||
pub mod post;
|
||||
pub mod quote_repost;
|
||||
pub mod reply;
|
||||
pub mod reply_description;
|
||||
|
||||
pub use contents::NoteContents;
|
||||
use contents::NoteContext;
|
||||
pub use context::{NoteContextButton, NoteContextSelection};
|
||||
use notedeck_ui::ImagePulseTint;
|
||||
pub use options::NoteOptions;
|
||||
pub use post::{NewPostAction, PostAction, PostResponse, PostType, PostView};
|
||||
pub use quote_repost::QuoteRepostView;
|
||||
pub use reply::PostReplyView;
|
||||
pub use reply_description::reply_desc;
|
||||
|
||||
use crate::{
|
||||
actionbar::{ContextSelection, NoteAction, ZapAction},
|
||||
profile::get_display_name,
|
||||
timeline::{ThreadSelection, TimelineKind},
|
||||
ui::{self, View},
|
||||
};
|
||||
|
||||
use egui::emath::{pos2, Vec2};
|
||||
use egui::{Id, Label, Pos2, Rect, Response, RichText, Sense};
|
||||
use enostr::{KeypairUnowned, NoteId, Pubkey};
|
||||
use nostrdb::{Ndb, Note, NoteKey, Transaction};
|
||||
use notedeck::{
|
||||
AnyZapState, CachedNote, NoteCache, NoteZapTarget, NoteZapTargetOwned, NotedeckTextStyle,
|
||||
ZapTarget, Zaps,
|
||||
};
|
||||
|
||||
use super::{profile::preview::one_line_display_name_widget, widgets::x_button};
|
||||
|
||||
pub struct NoteView<'a, 'd> {
|
||||
note_context: &'a mut NoteContext<'d>,
|
||||
cur_acc: &'a Option<KeypairUnowned<'a>>,
|
||||
parent: Option<NoteKey>,
|
||||
note: &'a nostrdb::Note<'a>,
|
||||
flags: NoteOptions,
|
||||
}
|
||||
|
||||
pub struct NoteResponse {
|
||||
pub response: egui::Response,
|
||||
pub action: Option<NoteAction>,
|
||||
}
|
||||
|
||||
impl NoteResponse {
|
||||
pub fn new(response: egui::Response) -> Self {
|
||||
Self {
|
||||
response,
|
||||
action: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_action(mut self, action: Option<NoteAction>) -> Self {
|
||||
self.action = action;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl View for NoteView<'_, '_> {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
self.show(ui);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'd> NoteView<'a, 'd> {
|
||||
pub fn new(
|
||||
note_context: &'a mut NoteContext<'d>,
|
||||
cur_acc: &'a Option<KeypairUnowned<'a>>,
|
||||
note: &'a nostrdb::Note<'a>,
|
||||
mut flags: NoteOptions,
|
||||
) -> Self {
|
||||
flags.set_actionbar(true);
|
||||
flags.set_note_previews(true);
|
||||
|
||||
let parent: Option<NoteKey> = None;
|
||||
Self {
|
||||
note_context,
|
||||
cur_acc,
|
||||
parent,
|
||||
note,
|
||||
flags,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn textmode(mut self, enable: bool) -> Self {
|
||||
self.options_mut().set_textmode(enable);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn actionbar(mut self, enable: bool) -> Self {
|
||||
self.options_mut().set_actionbar(enable);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn small_pfp(mut self, enable: bool) -> Self {
|
||||
self.options_mut().set_small_pfp(enable);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn medium_pfp(mut self, enable: bool) -> Self {
|
||||
self.options_mut().set_medium_pfp(enable);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn note_previews(mut self, enable: bool) -> Self {
|
||||
self.options_mut().set_note_previews(enable);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn selectable_text(mut self, enable: bool) -> Self {
|
||||
self.options_mut().set_selectable_text(enable);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn wide(mut self, enable: bool) -> Self {
|
||||
self.options_mut().set_wide(enable);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn options_button(mut self, enable: bool) -> Self {
|
||||
self.options_mut().set_options_button(enable);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn options(&self) -> NoteOptions {
|
||||
self.flags
|
||||
}
|
||||
|
||||
pub fn options_mut(&mut self) -> &mut NoteOptions {
|
||||
&mut self.flags
|
||||
}
|
||||
|
||||
pub fn parent(mut self, parent: NoteKey) -> Self {
|
||||
self.parent = Some(parent);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_preview(mut self, is_preview: bool) -> Self {
|
||||
self.options_mut().set_is_preview(is_preview);
|
||||
self
|
||||
}
|
||||
|
||||
fn textmode_ui(&mut self, ui: &mut egui::Ui) -> egui::Response {
|
||||
let note_key = self.note.key().expect("todo: implement non-db notes");
|
||||
let txn = self.note.txn().expect("todo: implement non-db notes");
|
||||
|
||||
ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| {
|
||||
let profile = self
|
||||
.note_context
|
||||
.ndb
|
||||
.get_profile_by_pubkey(txn, self.note.pubkey());
|
||||
|
||||
//ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 2.0;
|
||||
|
||||
let cached_note = self
|
||||
.note_context
|
||||
.note_cache
|
||||
.cached_note_or_insert_mut(note_key, self.note);
|
||||
|
||||
let (_id, rect) = ui.allocate_space(egui::vec2(50.0, 20.0));
|
||||
ui.allocate_rect(rect, Sense::hover());
|
||||
ui.put(rect, |ui: &mut egui::Ui| {
|
||||
render_reltime(ui, cached_note, false).response
|
||||
});
|
||||
let (_id, rect) = ui.allocate_space(egui::vec2(150.0, 20.0));
|
||||
ui.allocate_rect(rect, Sense::hover());
|
||||
ui.put(rect, |ui: &mut egui::Ui| {
|
||||
ui.add(
|
||||
ui::Username::new(profile.as_ref().ok(), self.note.pubkey())
|
||||
.abbreviated(6)
|
||||
.pk_colored(true),
|
||||
)
|
||||
});
|
||||
|
||||
ui.add(&mut NoteContents::new(
|
||||
self.note_context,
|
||||
self.cur_acc,
|
||||
txn,
|
||||
self.note,
|
||||
self.flags,
|
||||
));
|
||||
//});
|
||||
})
|
||||
.response
|
||||
}
|
||||
|
||||
pub fn expand_size() -> i8 {
|
||||
5
|
||||
}
|
||||
|
||||
fn pfp(
|
||||
&mut self,
|
||||
note_key: NoteKey,
|
||||
profile: &Result<nostrdb::ProfileRecord<'_>, nostrdb::Error>,
|
||||
ui: &mut egui::Ui,
|
||||
) -> egui::Response {
|
||||
if !self.options().has_wide() {
|
||||
ui.spacing_mut().item_spacing.x = 16.0;
|
||||
} else {
|
||||
ui.spacing_mut().item_spacing.x = 4.0;
|
||||
}
|
||||
|
||||
let pfp_size = self.options().pfp_size();
|
||||
|
||||
let sense = Sense::click();
|
||||
match profile
|
||||
.as_ref()
|
||||
.ok()
|
||||
.and_then(|p| p.record().profile()?.picture())
|
||||
{
|
||||
// these have different lifetimes and types,
|
||||
// so the calls must be separate
|
||||
Some(pic) => {
|
||||
let anim_speed = 0.05;
|
||||
let profile_key = profile.as_ref().unwrap().record().note_key();
|
||||
let note_key = note_key.as_u64();
|
||||
|
||||
let (rect, size, resp) = ui::anim::hover_expand(
|
||||
ui,
|
||||
egui::Id::new((profile_key, note_key)),
|
||||
pfp_size as f32,
|
||||
ui::NoteView::expand_size() as f32,
|
||||
anim_speed,
|
||||
);
|
||||
|
||||
ui.put(
|
||||
rect,
|
||||
ui::ProfilePic::new(self.note_context.img_cache, pic).size(size),
|
||||
)
|
||||
.on_hover_ui_at_pointer(|ui| {
|
||||
ui.set_max_width(300.0);
|
||||
ui.add(ui::ProfilePreview::new(
|
||||
profile.as_ref().unwrap(),
|
||||
self.note_context.img_cache,
|
||||
));
|
||||
});
|
||||
|
||||
if resp.hovered() || resp.clicked() {
|
||||
ui::show_pointer(ui);
|
||||
}
|
||||
|
||||
resp
|
||||
}
|
||||
|
||||
None => {
|
||||
// This has to match the expand size from the above case to
|
||||
// prevent bounciness
|
||||
let size = (pfp_size + ui::NoteView::expand_size()) as f32;
|
||||
let (rect, _response) = ui.allocate_exact_size(egui::vec2(size, size), sense);
|
||||
|
||||
ui.put(
|
||||
rect,
|
||||
ui::ProfilePic::new(self.note_context.img_cache, ui::ProfilePic::no_pfp_url())
|
||||
.size(pfp_size as f32),
|
||||
)
|
||||
.interact(sense)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(&mut self, ui: &mut egui::Ui) -> NoteResponse {
|
||||
if self.options().has_textmode() {
|
||||
NoteResponse::new(self.textmode_ui(ui))
|
||||
} else {
|
||||
let txn = self.note.txn().expect("txn");
|
||||
if let Some(note_to_repost) = get_reposted_note(self.note_context.ndb, txn, self.note) {
|
||||
let profile = self
|
||||
.note_context
|
||||
.ndb
|
||||
.get_profile_by_pubkey(txn, self.note.pubkey());
|
||||
|
||||
let style = NotedeckTextStyle::Small;
|
||||
ui.horizontal(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(2.0);
|
||||
ui.add_sized([20.0, 20.0], repost_icon(ui.visuals().dark_mode));
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
let resp = ui.add(one_line_display_name_widget(
|
||||
ui.visuals(),
|
||||
get_display_name(profile.as_ref().ok()),
|
||||
style,
|
||||
));
|
||||
if let Ok(rec) = &profile {
|
||||
resp.on_hover_ui_at_pointer(|ui| {
|
||||
ui.set_max_width(300.0);
|
||||
ui.add(ui::ProfilePreview::new(rec, self.note_context.img_cache));
|
||||
});
|
||||
}
|
||||
let color = ui.style().visuals.noninteractive().fg_stroke.color;
|
||||
ui.add_space(4.0);
|
||||
ui.label(
|
||||
RichText::new("Reposted")
|
||||
.color(color)
|
||||
.text_style(style.text_style()),
|
||||
);
|
||||
});
|
||||
NoteView::new(self.note_context, self.cur_acc, ¬e_to_repost, self.flags).show(ui)
|
||||
} else {
|
||||
self.show_standard(ui)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn note_header(
|
||||
ui: &mut egui::Ui,
|
||||
note_cache: &mut NoteCache,
|
||||
note: &Note,
|
||||
profile: &Result<nostrdb::ProfileRecord<'_>, nostrdb::Error>,
|
||||
) {
|
||||
let note_key = note.key().unwrap();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 2.0;
|
||||
ui.add(ui::Username::new(profile.as_ref().ok(), note.pubkey()).abbreviated(20));
|
||||
|
||||
let cached_note = note_cache.cached_note_or_insert_mut(note_key, note);
|
||||
render_reltime(ui, cached_note, true);
|
||||
});
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn show_standard(&mut self, ui: &mut egui::Ui) -> NoteResponse {
|
||||
let note_key = self.note.key().expect("todo: support non-db notes");
|
||||
let txn = self.note.txn().expect("todo: support non-db notes");
|
||||
|
||||
let mut note_action: Option<NoteAction> = None;
|
||||
|
||||
let hitbox_id = note_hitbox_id(note_key, self.options(), self.parent);
|
||||
let profile = self
|
||||
.note_context
|
||||
.ndb
|
||||
.get_profile_by_pubkey(txn, self.note.pubkey());
|
||||
let maybe_hitbox = maybe_note_hitbox(ui, hitbox_id);
|
||||
|
||||
// wide design
|
||||
let response = if self.options().has_wide() {
|
||||
ui.vertical(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
if self.pfp(note_key, &profile, ui).clicked() {
|
||||
note_action = Some(NoteAction::OpenTimeline(TimelineKind::profile(
|
||||
Pubkey::new(*self.note.pubkey()),
|
||||
)));
|
||||
};
|
||||
|
||||
let size = ui.available_size();
|
||||
ui.vertical(|ui| {
|
||||
ui.add_sized(
|
||||
[size.x, self.options().pfp_size() as f32],
|
||||
|ui: &mut egui::Ui| {
|
||||
ui.horizontal_centered(|ui| {
|
||||
NoteView::note_header(
|
||||
ui,
|
||||
self.note_context.note_cache,
|
||||
self.note,
|
||||
&profile,
|
||||
);
|
||||
})
|
||||
.response
|
||||
},
|
||||
);
|
||||
|
||||
let note_reply = self
|
||||
.note_context
|
||||
.note_cache
|
||||
.cached_note_or_insert_mut(note_key, self.note)
|
||||
.reply
|
||||
.borrow(self.note.tags());
|
||||
|
||||
if note_reply.reply().is_some() {
|
||||
let action = ui
|
||||
.horizontal(|ui| {
|
||||
reply_desc(
|
||||
ui,
|
||||
self.cur_acc,
|
||||
txn,
|
||||
¬e_reply,
|
||||
self.note_context,
|
||||
self.flags,
|
||||
)
|
||||
})
|
||||
.inner;
|
||||
|
||||
if action.is_some() {
|
||||
note_action = action;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let mut contents =
|
||||
NoteContents::new(self.note_context, self.cur_acc, txn, self.note, self.flags);
|
||||
|
||||
ui.add(&mut contents);
|
||||
|
||||
if let Some(action) = contents.action() {
|
||||
note_action = Some(action.clone());
|
||||
}
|
||||
|
||||
if self.options().has_actionbar() {
|
||||
if let Some(action) = render_note_actionbar(
|
||||
ui,
|
||||
self.note_context.zaps,
|
||||
self.cur_acc.as_ref(),
|
||||
self.note.id(),
|
||||
self.note.pubkey(),
|
||||
note_key,
|
||||
)
|
||||
.inner
|
||||
{
|
||||
note_action = Some(action);
|
||||
}
|
||||
}
|
||||
})
|
||||
.response
|
||||
} else {
|
||||
// main design
|
||||
ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| {
|
||||
if self.pfp(note_key, &profile, ui).clicked() {
|
||||
note_action = Some(NoteAction::OpenTimeline(TimelineKind::Profile(
|
||||
Pubkey::new(*self.note.pubkey()),
|
||||
)));
|
||||
};
|
||||
|
||||
ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| {
|
||||
NoteView::note_header(ui, self.note_context.note_cache, self.note, &profile);
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 2.0;
|
||||
|
||||
let note_reply = self
|
||||
.note_context
|
||||
.note_cache
|
||||
.cached_note_or_insert_mut(note_key, self.note)
|
||||
.reply
|
||||
.borrow(self.note.tags());
|
||||
|
||||
if note_reply.reply().is_some() {
|
||||
let action = reply_desc(
|
||||
ui,
|
||||
self.cur_acc,
|
||||
txn,
|
||||
¬e_reply,
|
||||
self.note_context,
|
||||
self.flags,
|
||||
);
|
||||
|
||||
if action.is_some() {
|
||||
note_action = action;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut contents = NoteContents::new(
|
||||
self.note_context,
|
||||
self.cur_acc,
|
||||
txn,
|
||||
self.note,
|
||||
self.flags,
|
||||
);
|
||||
ui.add(&mut contents);
|
||||
|
||||
if let Some(action) = contents.action() {
|
||||
note_action = Some(action.clone());
|
||||
}
|
||||
|
||||
if self.options().has_actionbar() {
|
||||
if let Some(action) = render_note_actionbar(
|
||||
ui,
|
||||
self.note_context.zaps,
|
||||
self.cur_acc.as_ref(),
|
||||
self.note.id(),
|
||||
self.note.pubkey(),
|
||||
note_key,
|
||||
)
|
||||
.inner
|
||||
{
|
||||
note_action = Some(action);
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
.response
|
||||
};
|
||||
|
||||
if self.options().has_options_button() {
|
||||
let context_pos = {
|
||||
let size = NoteContextButton::max_width();
|
||||
let top_right = response.rect.right_top();
|
||||
let min = Pos2::new(top_right.x - size, top_right.y);
|
||||
Rect::from_min_size(min, egui::vec2(size, size))
|
||||
};
|
||||
|
||||
let resp = ui.add(NoteContextButton::new(note_key).place_at(context_pos));
|
||||
if let Some(action) = NoteContextButton::menu(ui, resp.clone()) {
|
||||
note_action = Some(NoteAction::Context(ContextSelection { note_key, action }));
|
||||
}
|
||||
}
|
||||
|
||||
let note_action = if note_hitbox_clicked(ui, hitbox_id, &response.rect, maybe_hitbox) {
|
||||
if let Ok(selection) = ThreadSelection::from_note_id(
|
||||
self.note_context.ndb,
|
||||
self.note_context.note_cache,
|
||||
self.note.txn().unwrap(),
|
||||
NoteId::new(*self.note.id()),
|
||||
) {
|
||||
Some(NoteAction::OpenTimeline(TimelineKind::Thread(selection)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
note_action
|
||||
};
|
||||
|
||||
NoteResponse::new(response).with_action(note_action)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_reposted_note<'a>(ndb: &Ndb, txn: &'a Transaction, note: &Note) -> Option<Note<'a>> {
|
||||
let new_note_id: &[u8; 32] = if note.kind() == 6 {
|
||||
let mut res = None;
|
||||
for tag in note.tags().iter() {
|
||||
if tag.count() == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some("e") = tag.get(0).and_then(|t| t.variant().str()) {
|
||||
if let Some(note_id) = tag.get(1).and_then(|f| f.variant().id()) {
|
||||
res = Some(note_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
res?
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let note = ndb.get_note_by_id(txn, new_note_id).ok();
|
||||
note.filter(|note| note.kind() == 1)
|
||||
}
|
||||
|
||||
fn note_hitbox_id(
|
||||
note_key: NoteKey,
|
||||
note_options: NoteOptions,
|
||||
parent: Option<NoteKey>,
|
||||
) -> egui::Id {
|
||||
Id::new(("note_size", note_key, note_options, parent))
|
||||
}
|
||||
|
||||
fn maybe_note_hitbox(ui: &mut egui::Ui, hitbox_id: egui::Id) -> Option<Response> {
|
||||
ui.ctx()
|
||||
.data_mut(|d| d.get_persisted(hitbox_id))
|
||||
.map(|note_size: Vec2| {
|
||||
// The hitbox should extend the entire width of the
|
||||
// container. The hitbox height was cached last layout.
|
||||
let container_rect = ui.max_rect();
|
||||
let rect = Rect {
|
||||
min: pos2(container_rect.min.x, container_rect.min.y),
|
||||
max: pos2(container_rect.max.x, container_rect.min.y + note_size.y),
|
||||
};
|
||||
|
||||
let response = ui.interact(rect, ui.id().with(hitbox_id), egui::Sense::click());
|
||||
|
||||
response
|
||||
.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Other, true, "hitbox"));
|
||||
|
||||
response
|
||||
})
|
||||
}
|
||||
|
||||
fn note_hitbox_clicked(
|
||||
ui: &mut egui::Ui,
|
||||
hitbox_id: egui::Id,
|
||||
note_rect: &Rect,
|
||||
maybe_hitbox: Option<Response>,
|
||||
) -> bool {
|
||||
// Stash the dimensions of the note content so we can render the
|
||||
// hitbox in the next frame
|
||||
ui.ctx().data_mut(|d| {
|
||||
d.insert_persisted(hitbox_id, note_rect.size());
|
||||
});
|
||||
|
||||
// If there was an hitbox and it was clicked open the thread
|
||||
match maybe_hitbox {
|
||||
Some(hitbox) => hitbox.clicked(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn render_note_actionbar(
|
||||
ui: &mut egui::Ui,
|
||||
zaps: &Zaps,
|
||||
cur_acc: Option<&KeypairUnowned>,
|
||||
note_id: &[u8; 32],
|
||||
note_pubkey: &[u8; 32],
|
||||
note_key: NoteKey,
|
||||
) -> egui::InnerResponse<Option<NoteAction>> {
|
||||
ui.horizontal(|ui| 's: {
|
||||
let reply_resp = reply_button(ui, note_key);
|
||||
let quote_resp = quote_repost_button(ui, note_key);
|
||||
|
||||
let zap_target = ZapTarget::Note(NoteZapTarget {
|
||||
note_id,
|
||||
zap_recipient: note_pubkey,
|
||||
});
|
||||
|
||||
let zap_state = cur_acc.map_or_else(
|
||||
|| Ok(AnyZapState::None),
|
||||
|kp| zaps.any_zap_state_for(kp.pubkey.bytes(), zap_target),
|
||||
);
|
||||
let zap_resp = cur_acc
|
||||
.filter(|k| k.secret_key.is_some())
|
||||
.map(|_| match &zap_state {
|
||||
Ok(any_zap_state) => ui.add(zap_button(any_zap_state.clone(), note_id)),
|
||||
Err(zapping_error) => {
|
||||
let (rect, _) =
|
||||
ui.allocate_at_least(egui::vec2(10.0, 10.0), egui::Sense::click());
|
||||
ui.add(x_button(rect))
|
||||
.on_hover_text(format!("{zapping_error}"))
|
||||
}
|
||||
});
|
||||
|
||||
let to_noteid = |id: &[u8; 32]| NoteId::new(*id);
|
||||
|
||||
if reply_resp.clicked() {
|
||||
break 's Some(NoteAction::Reply(to_noteid(note_id)));
|
||||
}
|
||||
|
||||
if quote_resp.clicked() {
|
||||
break 's Some(NoteAction::Quote(to_noteid(note_id)));
|
||||
}
|
||||
|
||||
let Some(zap_resp) = zap_resp else {
|
||||
break 's None;
|
||||
};
|
||||
|
||||
if !zap_resp.clicked() {
|
||||
break 's None;
|
||||
}
|
||||
|
||||
let target = NoteZapTargetOwned {
|
||||
note_id: to_noteid(note_id),
|
||||
zap_recipient: Pubkey::new(*note_pubkey),
|
||||
};
|
||||
|
||||
if zap_state.is_err() {
|
||||
break 's Some(NoteAction::Zap(ZapAction::ClearError(target)));
|
||||
}
|
||||
|
||||
Some(NoteAction::Zap(ZapAction::Send(target)))
|
||||
})
|
||||
}
|
||||
|
||||
fn secondary_label(ui: &mut egui::Ui, s: impl Into<String>) {
|
||||
let color = ui.style().visuals.noninteractive().fg_stroke.color;
|
||||
ui.add(Label::new(RichText::new(s).size(10.0).color(color)));
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn render_reltime(
|
||||
ui: &mut egui::Ui,
|
||||
note_cache: &mut CachedNote,
|
||||
before: bool,
|
||||
) -> egui::InnerResponse<()> {
|
||||
ui.horizontal(|ui| {
|
||||
if before {
|
||||
secondary_label(ui, "⋅");
|
||||
}
|
||||
|
||||
secondary_label(ui, note_cache.reltime_str_mut());
|
||||
|
||||
if !before {
|
||||
secondary_label(ui, "⋅");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn reply_button(ui: &mut egui::Ui, note_key: NoteKey) -> egui::Response {
|
||||
let img_data = if ui.style().visuals.dark_mode {
|
||||
egui::include_image!("../../../../../assets/icons/reply.png")
|
||||
} else {
|
||||
egui::include_image!("../../../../../assets/icons/reply-dark.png")
|
||||
};
|
||||
|
||||
let (rect, size, resp) =
|
||||
ui::anim::hover_expand_small(ui, ui.id().with(("reply_anim", note_key)));
|
||||
|
||||
// align rect to note contents
|
||||
let expand_size = 5.0; // from hover_expand_small
|
||||
let rect = rect.translate(egui::vec2(-(expand_size / 2.0), 0.0));
|
||||
|
||||
let put_resp = ui.put(rect, egui::Image::new(img_data).max_width(size));
|
||||
|
||||
resp.union(put_resp)
|
||||
}
|
||||
|
||||
fn repost_icon(dark_mode: bool) -> egui::Image<'static> {
|
||||
let img_data = if dark_mode {
|
||||
egui::include_image!("../../../../../assets/icons/repost_icon_4x.png")
|
||||
} else {
|
||||
egui::include_image!("../../../../../assets/icons/repost_light_4x.png")
|
||||
};
|
||||
egui::Image::new(img_data)
|
||||
}
|
||||
|
||||
fn quote_repost_button(ui: &mut egui::Ui, note_key: NoteKey) -> egui::Response {
|
||||
let size = 14.0;
|
||||
let expand_size = 5.0;
|
||||
let anim_speed = 0.05;
|
||||
let id = ui.id().with(("repost_anim", note_key));
|
||||
|
||||
let (rect, size, resp) = ui::anim::hover_expand(ui, id, size, expand_size, anim_speed);
|
||||
|
||||
let rect = rect.translate(egui::vec2(-(expand_size / 2.0), -1.0));
|
||||
|
||||
let put_resp = ui.put(rect, repost_icon(ui.visuals().dark_mode).max_width(size));
|
||||
|
||||
resp.union(put_resp)
|
||||
}
|
||||
|
||||
fn zap_button(state: AnyZapState, noteid: &[u8; 32]) -> impl egui::Widget + use<'_> {
|
||||
move |ui: &mut egui::Ui| -> egui::Response {
|
||||
let img_data = egui::include_image!("../../../../../assets/icons/zap_4x.png");
|
||||
|
||||
let (rect, size, resp) = ui::anim::hover_expand_small(ui, ui.id().with("zap"));
|
||||
|
||||
let mut img = egui::Image::new(img_data).max_width(size);
|
||||
let id = ui.id().with(("pulse", noteid));
|
||||
let ctx = ui.ctx().clone();
|
||||
|
||||
match state {
|
||||
AnyZapState::None => {
|
||||
if !ui.visuals().dark_mode {
|
||||
img = img.tint(egui::Color32::BLACK);
|
||||
}
|
||||
}
|
||||
AnyZapState::Pending => {
|
||||
let alpha_min = if ui.visuals().dark_mode { 50 } else { 180 };
|
||||
img = ImagePulseTint::new(&ctx, id, img, &[0xFF, 0xB7, 0x57], alpha_min, 255)
|
||||
.with_speed(0.35)
|
||||
.animate();
|
||||
}
|
||||
AnyZapState::LocalOnly => {
|
||||
img = img.tint(egui::Color32::from_rgb(0xFF, 0xB7, 0x57));
|
||||
}
|
||||
AnyZapState::Confirmed => {}
|
||||
}
|
||||
|
||||
// align rect to note contents
|
||||
let expand_size = 5.0; // from hover_expand_small
|
||||
let rect = rect.translate(egui::vec2(-(expand_size / 2.0), 0.0));
|
||||
|
||||
let put_resp = ui.put(rect, img);
|
||||
|
||||
resp.union(put_resp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
use crate::ui::ProfilePic;
|
||||
use bitflags::bitflags;
|
||||
|
||||
bitflags! {
|
||||
// Attributes can be applied to flags types
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct NoteOptions: u64 {
|
||||
const actionbar = 0b0000000000000001;
|
||||
const note_previews = 0b0000000000000010;
|
||||
const small_pfp = 0b0000000000000100;
|
||||
const medium_pfp = 0b0000000000001000;
|
||||
const wide = 0b0000000000010000;
|
||||
const selectable_text = 0b0000000000100000;
|
||||
const textmode = 0b0000000001000000;
|
||||
const options_button = 0b0000000010000000;
|
||||
const hide_media = 0b0000000100000000;
|
||||
|
||||
/// Scramble text so that its not distracting during development
|
||||
const scramble_text = 0b0000001000000000;
|
||||
|
||||
/// Whether the current note is a preview
|
||||
const is_preview = 0b0000010000000000;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NoteOptions {
|
||||
fn default() -> NoteOptions {
|
||||
NoteOptions::options_button | NoteOptions::note_previews | NoteOptions::actionbar
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! create_bit_methods {
|
||||
($fn_name:ident, $has_name:ident, $option:ident) => {
|
||||
#[inline]
|
||||
pub fn $fn_name(&mut self, enable: bool) {
|
||||
if enable {
|
||||
*self |= NoteOptions::$option;
|
||||
} else {
|
||||
*self &= !NoteOptions::$option;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn $has_name(self) -> bool {
|
||||
(self & NoteOptions::$option) == NoteOptions::$option
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl NoteOptions {
|
||||
create_bit_methods!(set_small_pfp, has_small_pfp, small_pfp);
|
||||
create_bit_methods!(set_medium_pfp, has_medium_pfp, medium_pfp);
|
||||
create_bit_methods!(set_note_previews, has_note_previews, note_previews);
|
||||
create_bit_methods!(set_selectable_text, has_selectable_text, selectable_text);
|
||||
create_bit_methods!(set_textmode, has_textmode, textmode);
|
||||
create_bit_methods!(set_actionbar, has_actionbar, actionbar);
|
||||
create_bit_methods!(set_wide, has_wide, wide);
|
||||
create_bit_methods!(set_options_button, has_options_button, options_button);
|
||||
create_bit_methods!(set_hide_media, has_hide_media, hide_media);
|
||||
create_bit_methods!(set_scramble_text, has_scramble_text, scramble_text);
|
||||
create_bit_methods!(set_is_preview, has_is_preview, is_preview);
|
||||
|
||||
pub fn new(is_universe_timeline: bool) -> Self {
|
||||
let mut options = NoteOptions::default();
|
||||
options.set_hide_media(is_universe_timeline);
|
||||
options
|
||||
}
|
||||
|
||||
pub fn pfp_size(&self) -> i8 {
|
||||
if self.has_small_pfp() {
|
||||
ProfilePic::small_size()
|
||||
} else if self.has_medium_pfp() {
|
||||
ProfilePic::medium_size()
|
||||
} else {
|
||||
ProfilePic::default_size()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,29 @@
|
||||
use crate::actionbar::NoteAction;
|
||||
use crate::draft::{Draft, Drafts, MentionHint};
|
||||
use crate::media_upload::{nostrbuild_nip96_upload, MediaPath};
|
||||
use crate::post::{downcast_post_buffer, MentionType, NewPost};
|
||||
use crate::profile::get_display_name;
|
||||
use crate::ui::search_results::SearchResultsView;
|
||||
use crate::ui::{self, Preview, PreviewConfig};
|
||||
use crate::Result;
|
||||
use egui::text::{CCursorRange, LayoutJob};
|
||||
use egui::text_edit::TextEditOutput;
|
||||
use egui::widgets::text_edit::TextEdit;
|
||||
use egui::{vec2, Frame, Layout, Margin, Pos2, ScrollArea, Sense, TextBuffer};
|
||||
|
||||
use egui::{
|
||||
text::{CCursorRange, LayoutJob},
|
||||
text_edit::TextEditOutput,
|
||||
vec2,
|
||||
widgets::text_edit::TextEdit,
|
||||
Frame, Layout, Margin, Pos2, ScrollArea, Sense, TextBuffer,
|
||||
};
|
||||
use enostr::{FilledKeypair, FullKeypair, NoteId, Pubkey, RelayPool};
|
||||
use nostrdb::{Ndb, Transaction};
|
||||
use notedeck_ui::{
|
||||
gif::{handle_repaint, retrieve_latest_texture},
|
||||
images::render_images,
|
||||
note::render_note_preview,
|
||||
NoteOptions, ProfilePic,
|
||||
};
|
||||
|
||||
use notedeck::supported_mime_hosted_at_url;
|
||||
use notedeck::{name::get_display_name, supported_mime_hosted_at_url, NoteAction, NoteContext};
|
||||
use tracing::error;
|
||||
|
||||
use super::contents::{render_note_preview, NoteContext};
|
||||
use super::NoteOptions;
|
||||
|
||||
pub struct PostView<'a, 'd> {
|
||||
note_context: &'a mut NoteContext<'d>,
|
||||
draft: &'a mut Draft,
|
||||
@@ -133,14 +134,14 @@ impl<'a, 'd> PostView<'a, 'd> {
|
||||
.as_ref()
|
||||
.ok()
|
||||
.and_then(|p| {
|
||||
Some(ui::ProfilePic::from_profile(self.note_context.img_cache, p)?.size(pfp_size))
|
||||
Some(ProfilePic::from_profile(self.note_context.img_cache, p)?.size(pfp_size))
|
||||
});
|
||||
|
||||
if let Some(pfp) = poster_pfp {
|
||||
ui.add(pfp);
|
||||
} else {
|
||||
ui.add(
|
||||
ui::ProfilePic::new(self.note_context.img_cache, ui::ProfilePic::no_pfp_url())
|
||||
ProfilePic::new(self.note_context.img_cache, notedeck::profile::no_pfp_url())
|
||||
.size(pfp_size),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use enostr::{FilledKeypair, NoteId};
|
||||
|
||||
use super::{PostResponse, PostType};
|
||||
use crate::{
|
||||
draft::Draft,
|
||||
ui::{self},
|
||||
};
|
||||
|
||||
use super::{contents::NoteContext, NoteOptions, PostResponse, PostType};
|
||||
use enostr::{FilledKeypair, NoteId};
|
||||
use notedeck::NoteContext;
|
||||
use notedeck_ui::NoteOptions;
|
||||
|
||||
pub struct QuoteRepostView<'a, 'd> {
|
||||
note_context: &'a mut NoteContext<'d>,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use crate::draft::Draft;
|
||||
use crate::ui;
|
||||
use crate::ui::note::{PostAction, PostResponse, PostType};
|
||||
use enostr::{FilledKeypair, NoteId};
|
||||
use crate::ui::{
|
||||
self,
|
||||
note::{PostAction, PostResponse, PostType},
|
||||
};
|
||||
|
||||
use super::contents::NoteContext;
|
||||
use super::NoteOptions;
|
||||
use enostr::{FilledKeypair, NoteId};
|
||||
use notedeck::NoteContext;
|
||||
use notedeck_ui::{NoteOptions, NoteView, ProfilePic};
|
||||
|
||||
pub struct PostReplyView<'a, 'd> {
|
||||
note_context: &'a mut NoteContext<'d>,
|
||||
@@ -56,15 +58,15 @@ impl<'a, 'd> PostReplyView<'a, 'd> {
|
||||
// to indent things so that the reply line is aligned
|
||||
let pfp_offset: i8 = ui::PostView::outer_margin()
|
||||
+ ui::PostView::inner_margin()
|
||||
+ ui::ProfilePic::small_size() / 2;
|
||||
+ ProfilePic::small_size() / 2;
|
||||
|
||||
let note_offset: i8 =
|
||||
pfp_offset - ui::ProfilePic::medium_size() / 2 - ui::NoteView::expand_size() / 2;
|
||||
pfp_offset - ProfilePic::medium_size() / 2 - NoteView::expand_size() / 2;
|
||||
|
||||
let quoted_note = egui::Frame::NONE
|
||||
.outer_margin(egui::Margin::same(note_offset))
|
||||
.show(ui, |ui| {
|
||||
ui::NoteView::new(
|
||||
NoteView::new(
|
||||
self.note_context,
|
||||
&Some(self.poster.into()),
|
||||
self.note,
|
||||
@@ -113,9 +115,9 @@ impl<'a, 'd> PostReplyView<'a, 'd> {
|
||||
// honestly don't know what the fuck I'm doing here. just trying
|
||||
// to get the line under the profile picture
|
||||
rect.min.y = avail_rect.min.y
|
||||
+ (ui::ProfilePic::medium_size() as f32 / 2.0
|
||||
+ ui::ProfilePic::medium_size() as f32
|
||||
+ ui::NoteView::expand_size() as f32 * 2.0)
|
||||
+ (ProfilePic::medium_size() as f32 / 2.0
|
||||
+ ProfilePic::medium_size() as f32
|
||||
+ NoteView::expand_size() as f32 * 2.0)
|
||||
+ 1.0;
|
||||
|
||||
// For some reason we need to nudge the reply line's height a
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
use crate::{
|
||||
actionbar::NoteAction,
|
||||
ui::{self},
|
||||
};
|
||||
use egui::{Label, RichText, Sense};
|
||||
use enostr::KeypairUnowned;
|
||||
use nostrdb::{Note, NoteReply, Transaction};
|
||||
|
||||
use super::{contents::NoteContext, NoteOptions};
|
||||
|
||||
#[must_use = "Please handle the resulting note action"]
|
||||
#[profiling::function]
|
||||
pub fn reply_desc(
|
||||
ui: &mut egui::Ui,
|
||||
cur_acc: &Option<KeypairUnowned>,
|
||||
txn: &Transaction,
|
||||
note_reply: &NoteReply,
|
||||
note_context: &mut NoteContext,
|
||||
note_options: NoteOptions,
|
||||
) -> Option<NoteAction> {
|
||||
let mut note_action: Option<NoteAction> = None;
|
||||
let size = 10.0;
|
||||
let selectable = false;
|
||||
let visuals = ui.visuals();
|
||||
let color = visuals.noninteractive().fg_stroke.color;
|
||||
let link_color = visuals.hyperlink_color;
|
||||
|
||||
// note link renderer helper
|
||||
let note_link =
|
||||
|ui: &mut egui::Ui, note_context: &mut NoteContext, text: &str, note: &Note<'_>| {
|
||||
let r = ui.add(
|
||||
Label::new(RichText::new(text).size(size).color(link_color))
|
||||
.sense(Sense::click())
|
||||
.selectable(selectable),
|
||||
);
|
||||
|
||||
if r.clicked() {
|
||||
// TODO: jump to note
|
||||
}
|
||||
|
||||
if r.hovered() {
|
||||
r.on_hover_ui_at_pointer(|ui| {
|
||||
ui.set_max_width(400.0);
|
||||
ui::NoteView::new(note_context, cur_acc, note, note_options)
|
||||
.actionbar(false)
|
||||
.wide(true)
|
||||
.show(ui);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
ui.add(Label::new(RichText::new("replying to").size(size).color(color)).selectable(selectable));
|
||||
|
||||
let reply = note_reply.reply()?;
|
||||
|
||||
let reply_note = if let Ok(reply_note) = note_context.ndb.get_note_by_id(txn, reply.id) {
|
||||
reply_note
|
||||
} else {
|
||||
ui.add(Label::new(RichText::new("a note").size(size).color(color)).selectable(selectable));
|
||||
return None;
|
||||
};
|
||||
|
||||
if note_reply.is_reply_to_root() {
|
||||
// We're replying to the root, let's show this
|
||||
let action = ui::Mention::new(
|
||||
note_context.ndb,
|
||||
note_context.img_cache,
|
||||
txn,
|
||||
reply_note.pubkey(),
|
||||
)
|
||||
.size(size)
|
||||
.selectable(selectable)
|
||||
.show(ui)
|
||||
.inner;
|
||||
|
||||
if action.is_some() {
|
||||
note_action = action;
|
||||
}
|
||||
|
||||
ui.add(Label::new(RichText::new("'s").size(size).color(color)).selectable(selectable));
|
||||
|
||||
note_link(ui, note_context, "thread", &reply_note);
|
||||
} else if let Some(root) = note_reply.root() {
|
||||
// replying to another post in a thread, not the root
|
||||
|
||||
if let Ok(root_note) = note_context.ndb.get_note_by_id(txn, root.id) {
|
||||
if root_note.pubkey() == reply_note.pubkey() {
|
||||
// simply "replying to bob's note" when replying to bob in his thread
|
||||
let action = ui::Mention::new(
|
||||
note_context.ndb,
|
||||
note_context.img_cache,
|
||||
txn,
|
||||
reply_note.pubkey(),
|
||||
)
|
||||
.size(size)
|
||||
.selectable(selectable)
|
||||
.show(ui)
|
||||
.inner;
|
||||
|
||||
if action.is_some() {
|
||||
note_action = action;
|
||||
}
|
||||
|
||||
ui.add(
|
||||
Label::new(RichText::new("'s").size(size).color(color)).selectable(selectable),
|
||||
);
|
||||
|
||||
note_link(ui, note_context, "note", &reply_note);
|
||||
} else {
|
||||
// replying to bob in alice's thread
|
||||
|
||||
let action = ui::Mention::new(
|
||||
note_context.ndb,
|
||||
note_context.img_cache,
|
||||
txn,
|
||||
reply_note.pubkey(),
|
||||
)
|
||||
.size(size)
|
||||
.selectable(selectable)
|
||||
.show(ui)
|
||||
.inner;
|
||||
|
||||
if action.is_some() {
|
||||
note_action = action;
|
||||
}
|
||||
|
||||
ui.add(
|
||||
Label::new(RichText::new("'s").size(size).color(color)).selectable(selectable),
|
||||
);
|
||||
|
||||
note_link(ui, note_context, "note", &reply_note);
|
||||
|
||||
ui.add(
|
||||
Label::new(RichText::new("in").size(size).color(color)).selectable(selectable),
|
||||
);
|
||||
|
||||
let action = ui::Mention::new(
|
||||
note_context.ndb,
|
||||
note_context.img_cache,
|
||||
txn,
|
||||
root_note.pubkey(),
|
||||
)
|
||||
.size(size)
|
||||
.selectable(selectable)
|
||||
.show(ui)
|
||||
.inner;
|
||||
|
||||
if action.is_some() {
|
||||
note_action = action;
|
||||
}
|
||||
|
||||
ui.add(
|
||||
Label::new(RichText::new("'s").size(size).color(color)).selectable(selectable),
|
||||
);
|
||||
|
||||
note_link(ui, note_context, "thread", &root_note);
|
||||
}
|
||||
} else {
|
||||
let action = ui::Mention::new(
|
||||
note_context.ndb,
|
||||
note_context.img_cache,
|
||||
txn,
|
||||
reply_note.pubkey(),
|
||||
)
|
||||
.size(size)
|
||||
.selectable(selectable)
|
||||
.show(ui)
|
||||
.inner;
|
||||
|
||||
if action.is_some() {
|
||||
note_action = action;
|
||||
}
|
||||
|
||||
ui.add(
|
||||
Label::new(RichText::new("in someone's thread").size(size).color(color))
|
||||
.selectable(selectable),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
note_action
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
use core::f32;
|
||||
|
||||
use egui::{vec2, Button, CornerRadius, Layout, Margin, RichText, ScrollArea, TextEdit};
|
||||
use notedeck::{Images, NotedeckTextStyle};
|
||||
|
||||
use crate::profile_state::ProfileState;
|
||||
|
||||
use super::banner;
|
||||
|
||||
use notedeck_ui::{profile::unwrap_profile_url, ProfilePic};
|
||||
use egui::{vec2, Button, CornerRadius, Layout, Margin, RichText, ScrollArea, TextEdit};
|
||||
use notedeck::{profile::unwrap_profile_url, Images, NotedeckTextStyle};
|
||||
use notedeck_ui::{profile::banner, ProfilePic};
|
||||
|
||||
pub struct EditProfileView<'a> {
|
||||
state: &'a mut ProfileState,
|
||||
@@ -26,14 +22,14 @@ impl<'a> EditProfileView<'a> {
|
||||
banner(ui, Some(&self.state.banner), 188.0);
|
||||
|
||||
let padding = 24.0;
|
||||
crate::ui::padding(padding, ui, |ui| {
|
||||
notedeck_ui::padding(padding, ui, |ui| {
|
||||
self.inner(ui, padding);
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
let mut save = false;
|
||||
crate::ui::padding(padding, ui, |ui| {
|
||||
notedeck_ui::padding(padding, ui, |ui| {
|
||||
ui.with_layout(Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if ui
|
||||
.add(button("Save changes", 119.0).fill(notedeck_ui::colors::PINK))
|
||||
|
||||
@@ -1,27 +1,23 @@
|
||||
pub mod edit;
|
||||
pub mod preview;
|
||||
|
||||
pub use edit::EditProfileView;
|
||||
use egui::load::TexturePoll;
|
||||
use egui::{vec2, Color32, CornerRadius, Label, Layout, Rect, RichText, ScrollArea, Sense, Stroke};
|
||||
use egui::{vec2, Color32, CornerRadius, Layout, Rect, RichText, ScrollArea, Sense, Stroke};
|
||||
use enostr::Pubkey;
|
||||
use nostrdb::{ProfileRecord, Transaction};
|
||||
pub use preview::ProfilePreview;
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
actionbar::NoteAction,
|
||||
profile::get_display_name,
|
||||
timeline::{TimelineCache, TimelineKind},
|
||||
ui::timeline::{tabs_ui, TimelineTabView},
|
||||
NostrName,
|
||||
};
|
||||
|
||||
use notedeck::{Accounts, MuteFun, NotedeckTextStyle, UnknownIds};
|
||||
use notedeck_ui::{images, profile::get_profile_url, ProfilePic};
|
||||
|
||||
use super::note::contents::NoteContext;
|
||||
use super::note::NoteOptions;
|
||||
use notedeck::{
|
||||
name::get_display_name, profile::get_profile_url, Accounts, MuteFun, NoteAction, NoteContext,
|
||||
NotedeckTextStyle, UnknownIds,
|
||||
};
|
||||
use notedeck_ui::{
|
||||
profile::{about_section_widget, banner, display_name_widget},
|
||||
NoteOptions, ProfilePic,
|
||||
};
|
||||
|
||||
pub struct ProfileView<'a, 'd> {
|
||||
pubkey: &'a Pubkey,
|
||||
@@ -137,7 +133,7 @@ impl<'a, 'd> ProfileView<'a, 'd> {
|
||||
);
|
||||
|
||||
let padding = 12.0;
|
||||
crate::ui::padding(padding, ui, |ui| {
|
||||
notedeck_ui::padding(padding, ui, |ui| {
|
||||
let mut pfp_rect = ui.available_rect_before_wrap();
|
||||
let size = 80.0;
|
||||
pfp_rect.set_width(size);
|
||||
@@ -342,110 +338,3 @@ fn edit_profile_button() -> impl egui::Widget + 'static {
|
||||
resp
|
||||
}
|
||||
}
|
||||
|
||||
fn display_name_widget<'a>(
|
||||
name: &'a NostrName<'a>,
|
||||
add_placeholder_space: bool,
|
||||
) -> impl egui::Widget + 'a {
|
||||
move |ui: &mut egui::Ui| -> egui::Response {
|
||||
let disp_resp = name.display_name.map(|disp_name| {
|
||||
ui.add(
|
||||
Label::new(
|
||||
RichText::new(disp_name).text_style(NotedeckTextStyle::Heading3.text_style()),
|
||||
)
|
||||
.selectable(false),
|
||||
)
|
||||
});
|
||||
|
||||
let (username_resp, nip05_resp) = ui
|
||||
.horizontal(|ui| {
|
||||
let username_resp = name.username.map(|username| {
|
||||
ui.add(
|
||||
Label::new(
|
||||
RichText::new(format!("@{}", username))
|
||||
.size(16.0)
|
||||
.color(notedeck_ui::colors::MID_GRAY),
|
||||
)
|
||||
.selectable(false),
|
||||
)
|
||||
});
|
||||
|
||||
let nip05_resp = name.nip05.map(|nip05| {
|
||||
ui.image(egui::include_image!(
|
||||
"../../../../../assets/icons/verified_4x.png"
|
||||
));
|
||||
ui.add(Label::new(
|
||||
RichText::new(nip05)
|
||||
.size(16.0)
|
||||
.color(notedeck_ui::colors::TEAL),
|
||||
))
|
||||
});
|
||||
|
||||
(username_resp, nip05_resp)
|
||||
})
|
||||
.inner;
|
||||
|
||||
let resp = match (disp_resp, username_resp, nip05_resp) {
|
||||
(Some(disp), Some(username), Some(nip05)) => disp.union(username).union(nip05),
|
||||
(Some(disp), Some(username), None) => disp.union(username),
|
||||
(Some(disp), None, None) => disp,
|
||||
(None, Some(username), Some(nip05)) => username.union(nip05),
|
||||
(None, Some(username), None) => username,
|
||||
_ => ui.add(Label::new(RichText::new(name.name()))),
|
||||
};
|
||||
|
||||
if add_placeholder_space {
|
||||
ui.add_space(16.0);
|
||||
}
|
||||
|
||||
resp
|
||||
}
|
||||
}
|
||||
|
||||
fn about_section_widget<'a, 'b>(profile: &'b ProfileRecord<'a>) -> impl egui::Widget + 'b
|
||||
where
|
||||
'b: 'a,
|
||||
{
|
||||
move |ui: &mut egui::Ui| {
|
||||
if let Some(about) = profile.record().profile().and_then(|p| p.about()) {
|
||||
let resp = ui.label(about);
|
||||
ui.add_space(8.0);
|
||||
resp
|
||||
} else {
|
||||
// need any Response so we dont need an Option
|
||||
ui.allocate_response(egui::Vec2::ZERO, egui::Sense::hover())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn banner_texture(ui: &mut egui::Ui, banner_url: &str) -> Option<egui::load::SizedTexture> {
|
||||
// TODO: cache banner
|
||||
if !banner_url.is_empty() {
|
||||
let texture_load_res =
|
||||
egui::Image::new(banner_url).load_for_size(ui.ctx(), ui.available_size());
|
||||
if let Ok(texture_poll) = texture_load_res {
|
||||
match texture_poll {
|
||||
TexturePoll::Pending { .. } => {}
|
||||
TexturePoll::Ready { texture, .. } => return Some(texture),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn banner(ui: &mut egui::Ui, banner_url: Option<&str>, height: f32) -> egui::Response {
|
||||
ui.add_sized([ui.available_size().x, height], |ui: &mut egui::Ui| {
|
||||
banner_url
|
||||
.and_then(|url| banner_texture(ui, url))
|
||||
.map(|texture| {
|
||||
images::aspect_fill(
|
||||
ui,
|
||||
Sense::hover(),
|
||||
texture.id,
|
||||
texture.size.x / texture.size.y,
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| ui.label(""))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
use crate::ui::ProfilePic;
|
||||
use crate::NostrName;
|
||||
use egui::{Frame, Label, RichText, Widget};
|
||||
use egui_extras::Size;
|
||||
use nostrdb::ProfileRecord;
|
||||
|
||||
use notedeck::{Images, NotedeckTextStyle};
|
||||
|
||||
use super::{about_section_widget, banner, display_name_widget, get_display_name};
|
||||
use notedeck_ui::profile::get_profile_url;
|
||||
|
||||
pub struct ProfilePreview<'a, 'cache> {
|
||||
profile: &'a ProfileRecord<'a>,
|
||||
cache: &'cache mut Images,
|
||||
banner_height: Size,
|
||||
}
|
||||
|
||||
impl<'a, 'cache> ProfilePreview<'a, 'cache> {
|
||||
pub fn new(profile: &'a ProfileRecord<'a>, cache: &'cache mut Images) -> Self {
|
||||
let banner_height = Size::exact(80.0);
|
||||
ProfilePreview {
|
||||
profile,
|
||||
cache,
|
||||
banner_height,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn banner_height(&mut self, size: Size) {
|
||||
self.banner_height = size;
|
||||
}
|
||||
|
||||
fn body(self, ui: &mut egui::Ui) {
|
||||
let padding = 12.0;
|
||||
crate::ui::padding(padding, ui, |ui| {
|
||||
let mut pfp_rect = ui.available_rect_before_wrap();
|
||||
let size = 80.0;
|
||||
pfp_rect.set_width(size);
|
||||
pfp_rect.set_height(size);
|
||||
let pfp_rect = pfp_rect.translate(egui::vec2(0.0, -(padding + 2.0 + (size / 2.0))));
|
||||
|
||||
ui.put(
|
||||
pfp_rect,
|
||||
ProfilePic::new(self.cache, get_profile_url(Some(self.profile)))
|
||||
.size(size)
|
||||
.border(ProfilePic::border_stroke(ui)),
|
||||
);
|
||||
ui.add(display_name_widget(
|
||||
&get_display_name(Some(self.profile)),
|
||||
false,
|
||||
));
|
||||
ui.add(about_section_widget(self.profile));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl egui::Widget for ProfilePreview<'_, '_> {
|
||||
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
|
||||
ui.vertical(|ui| {
|
||||
banner(
|
||||
ui,
|
||||
self.profile.record().profile().and_then(|p| p.banner()),
|
||||
80.0,
|
||||
);
|
||||
|
||||
self.body(ui);
|
||||
})
|
||||
.response
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SimpleProfilePreview<'a, 'cache> {
|
||||
profile: Option<&'a ProfileRecord<'a>>,
|
||||
cache: &'cache mut Images,
|
||||
is_nsec: bool,
|
||||
}
|
||||
|
||||
impl<'a, 'cache> SimpleProfilePreview<'a, 'cache> {
|
||||
pub fn new(
|
||||
profile: Option<&'a ProfileRecord<'a>>,
|
||||
cache: &'cache mut Images,
|
||||
is_nsec: bool,
|
||||
) -> Self {
|
||||
SimpleProfilePreview {
|
||||
profile,
|
||||
cache,
|
||||
is_nsec,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl egui::Widget for SimpleProfilePreview<'_, '_> {
|
||||
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
|
||||
Frame::new()
|
||||
.show(ui, |ui| {
|
||||
ui.add(ProfilePic::new(self.cache, get_profile_url(self.profile)).size(48.0));
|
||||
ui.vertical(|ui| {
|
||||
ui.add(display_name_widget(&get_display_name(self.profile), true));
|
||||
if !self.is_nsec {
|
||||
ui.add(
|
||||
Label::new(
|
||||
RichText::new("Read only")
|
||||
.size(notedeck::fonts::get_font_size(
|
||||
ui.ctx(),
|
||||
&NotedeckTextStyle::Tiny,
|
||||
))
|
||||
.color(ui.visuals().warn_fg_color),
|
||||
)
|
||||
.selectable(false),
|
||||
);
|
||||
}
|
||||
});
|
||||
})
|
||||
.response
|
||||
}
|
||||
}
|
||||
|
||||
mod previews {
|
||||
use super::*;
|
||||
use crate::test_data::test_profile_record;
|
||||
use crate::ui::{Preview, PreviewConfig};
|
||||
use notedeck::{App, AppContext};
|
||||
|
||||
pub struct ProfilePreviewPreview<'a> {
|
||||
profile: ProfileRecord<'a>,
|
||||
}
|
||||
|
||||
impl ProfilePreviewPreview<'_> {
|
||||
pub fn new() -> Self {
|
||||
let profile = test_profile_record();
|
||||
ProfilePreviewPreview { profile }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ProfilePreviewPreview<'_> {
|
||||
fn default() -> Self {
|
||||
ProfilePreviewPreview::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl App for ProfilePreviewPreview<'_> {
|
||||
fn update(&mut self, app: &mut AppContext<'_>, ui: &mut egui::Ui) {
|
||||
ProfilePreview::new(&self.profile, app.img_cache).ui(ui);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Preview for ProfilePreview<'a, '_> {
|
||||
/// A preview of the profile preview :D
|
||||
type Prev = ProfilePreviewPreview<'a>;
|
||||
|
||||
fn preview(_cfg: PreviewConfig) -> Self::Prev {
|
||||
ProfilePreviewPreview::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn one_line_display_name_widget<'a>(
|
||||
visuals: &egui::Visuals,
|
||||
display_name: NostrName<'a>,
|
||||
style: NotedeckTextStyle,
|
||||
) -> impl egui::Widget + 'a {
|
||||
let text_style = style.text_style();
|
||||
let color = visuals.noninteractive().fg_stroke.color;
|
||||
|
||||
move |ui: &mut egui::Ui| -> egui::Response {
|
||||
ui.label(
|
||||
RichText::new(display_name.name())
|
||||
.text_style(text_style)
|
||||
.color(color),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,15 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::relay_pool_manager::{RelayPoolManager, RelayStatus};
|
||||
use crate::ui::{Preview, PreviewConfig, View};
|
||||
use crate::ui::{Preview, PreviewConfig};
|
||||
use egui::{
|
||||
Align, Button, CornerRadius, Frame, Id, Image, Layout, Margin, Rgba, RichText, Ui, Vec2,
|
||||
};
|
||||
use notedeck_ui::colors::PINK;
|
||||
|
||||
use enostr::RelayPool;
|
||||
use notedeck::{Accounts, NotedeckTextStyle};
|
||||
|
||||
use notedeck_ui::{colors::PINK, padding, View};
|
||||
use tracing::debug;
|
||||
|
||||
use super::padding;
|
||||
use super::widgets::styled_button;
|
||||
|
||||
pub struct RelayView<'a> {
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
use egui::{vec2, Align, Color32, CornerRadius, RichText, Stroke, TextEdit};
|
||||
use enostr::KeypairUnowned;
|
||||
|
||||
use super::{note::contents::NoteContext, padding};
|
||||
use crate::{
|
||||
actionbar::NoteAction,
|
||||
ui::{note::NoteOptions, timeline::TimelineTabView},
|
||||
};
|
||||
use crate::ui::timeline::TimelineTabView;
|
||||
use egui_winit::clipboard::Clipboard;
|
||||
use nostrdb::{Filter, Transaction};
|
||||
use notedeck::{MuteFun, NoteRef};
|
||||
use notedeck_ui::icons::search_icon;
|
||||
use notedeck::{MuteFun, NoteAction, NoteContext, NoteRef};
|
||||
use notedeck_ui::{icons::search_icon, padding, NoteOptions};
|
||||
use std::time::{Duration, Instant};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use egui::{vec2, FontId, Layout, Pos2, Rect, ScrollArea, UiBuilder, Vec2b};
|
||||
use nostrdb::{Ndb, ProfileRecord, Transaction};
|
||||
use notedeck::{fonts::get_font_size, Images, NotedeckTextStyle};
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
profile::get_display_name,
|
||||
ui::anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
|
||||
use notedeck::{
|
||||
fonts::get_font_size, name::get_display_name, profile::get_profile_url, Images,
|
||||
NotedeckTextStyle,
|
||||
};
|
||||
|
||||
use super::{widgets::x_button, ProfilePic};
|
||||
use notedeck_ui::profile::get_profile_url;
|
||||
use notedeck_ui::{
|
||||
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
|
||||
widgets::x_button,
|
||||
ProfilePic,
|
||||
};
|
||||
use tracing::error;
|
||||
|
||||
pub struct SearchResultsView<'a> {
|
||||
ndb: &'a Ndb,
|
||||
|
||||
@@ -12,14 +12,13 @@ use crate::{
|
||||
};
|
||||
|
||||
use notedeck::{Accounts, UserAccount};
|
||||
use notedeck_ui::colors;
|
||||
|
||||
use super::{
|
||||
use notedeck_ui::{
|
||||
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
|
||||
configure_deck::deck_icon,
|
||||
View,
|
||||
colors, View,
|
||||
};
|
||||
|
||||
use super::configure_deck::deck_icon;
|
||||
|
||||
pub static SIDE_PANEL_WIDTH: f32 = 68.0;
|
||||
static ICON_WIDTH: f32 = 40.0;
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
use egui::{vec2, Button, Label, Layout, RichText};
|
||||
use notedeck::{NamedFontFamily, NotedeckTextStyle};
|
||||
use notedeck_ui::{colors::PINK, padding};
|
||||
use tracing::error;
|
||||
|
||||
use crate::support::Support;
|
||||
use notedeck_ui::colors::PINK;
|
||||
|
||||
use super::padding;
|
||||
use notedeck::{NamedFontFamily, NotedeckTextStyle};
|
||||
|
||||
pub struct SupportView<'a> {
|
||||
support: &'a mut Support,
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
use crate::{
|
||||
actionbar::NoteAction,
|
||||
timeline::{ThreadSelection, TimelineCache, TimelineKind},
|
||||
};
|
||||
|
||||
use enostr::KeypairUnowned;
|
||||
use nostrdb::Transaction;
|
||||
use notedeck::{MuteFun, RootNoteId, UnknownIds};
|
||||
use notedeck::{MuteFun, NoteAction, NoteContext, RootNoteId, UnknownIds};
|
||||
use notedeck_ui::NoteOptions;
|
||||
use tracing::error;
|
||||
|
||||
use super::{
|
||||
note::{contents::NoteContext, NoteOptions},
|
||||
timeline::TimelineTabView,
|
||||
};
|
||||
use crate::timeline::{ThreadSelection, TimelineCache, TimelineKind};
|
||||
use crate::ui::timeline::TimelineTabView;
|
||||
|
||||
pub struct ThreadView<'a, 'd> {
|
||||
timeline_cache: &'a mut TimelineCache,
|
||||
|
||||
@@ -1,23 +1,17 @@
|
||||
use std::f32::consts::PI;
|
||||
|
||||
use crate::actionbar::NoteAction;
|
||||
use crate::timeline::TimelineTab;
|
||||
use crate::{
|
||||
timeline::{TimelineCache, TimelineKind, ViewFilter},
|
||||
ui,
|
||||
};
|
||||
use egui::containers::scroll_area::ScrollBarVisibility;
|
||||
use egui::{vec2, Direction, Layout, Pos2, Stroke};
|
||||
use egui_tabs::TabColor;
|
||||
use enostr::KeypairUnowned;
|
||||
use nostrdb::Transaction;
|
||||
use notedeck::note::root_note_id_from_selected_id;
|
||||
use notedeck::MuteFun;
|
||||
use std::f32::consts::PI;
|
||||
use tracing::{error, warn};
|
||||
|
||||
use super::anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE};
|
||||
use super::note::contents::NoteContext;
|
||||
use super::note::NoteOptions;
|
||||
use crate::timeline::{TimelineCache, TimelineKind, TimelineTab, ViewFilter};
|
||||
use notedeck::{note::root_note_id_from_selected_id, MuteFun, NoteAction, NoteContext};
|
||||
use notedeck_ui::{
|
||||
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
|
||||
show_pointer, NoteOptions, NoteView,
|
||||
};
|
||||
|
||||
pub struct TimelineView<'a, 'd> {
|
||||
timeline_id: &'a TimelineKind,
|
||||
@@ -134,7 +128,7 @@ fn timeline_ui(
|
||||
if goto_top_resp.clicked() {
|
||||
scroll_area = scroll_area.vertical_scroll_offset(0.0);
|
||||
} else if goto_top_resp.hovered() {
|
||||
ui::show_pointer(ui);
|
||||
show_pointer(ui);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,7 +265,7 @@ pub fn tabs_ui(ui: &mut egui::Ui, selected: usize, views: &[TimelineTab]) -> usi
|
||||
});
|
||||
|
||||
//ui.add_space(0.5);
|
||||
ui::hline(ui);
|
||||
notedeck_ui::hline(ui);
|
||||
|
||||
let sel = tab_res.selected().unwrap_or_default();
|
||||
|
||||
@@ -395,8 +389,8 @@ impl<'a, 'd> TimelineTabView<'a, 'd> {
|
||||
};
|
||||
|
||||
if !muted {
|
||||
ui::padding(8.0, ui, |ui| {
|
||||
let resp = ui::NoteView::new(
|
||||
notedeck_ui::padding(8.0, ui, |ui| {
|
||||
let resp = NoteView::new(
|
||||
self.note_context,
|
||||
self.cur_acc,
|
||||
¬e,
|
||||
@@ -409,7 +403,7 @@ impl<'a, 'd> TimelineTabView<'a, 'd> {
|
||||
}
|
||||
});
|
||||
|
||||
ui::hline(ui);
|
||||
notedeck_ui::hline(ui);
|
||||
}
|
||||
|
||||
1
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
use egui::{Color32, RichText, Widget};
|
||||
use nostrdb::ProfileRecord;
|
||||
use notedeck::fonts::NamedFontFamily;
|
||||
|
||||
pub struct Username<'a> {
|
||||
profile: Option<&'a ProfileRecord<'a>>,
|
||||
pk: &'a [u8; 32],
|
||||
pk_colored: bool,
|
||||
abbrev: usize,
|
||||
}
|
||||
|
||||
impl<'a> Username<'a> {
|
||||
pub fn pk_colored(mut self, pk_colored: bool) -> Self {
|
||||
self.pk_colored = pk_colored;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn abbreviated(mut self, amount: usize) -> Self {
|
||||
self.abbrev = amount;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn new(profile: Option<&'a ProfileRecord>, pk: &'a [u8; 32]) -> Self {
|
||||
let pk_colored = false;
|
||||
let abbrev: usize = 1000;
|
||||
Username {
|
||||
profile,
|
||||
pk,
|
||||
pk_colored,
|
||||
abbrev,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Username<'_> {
|
||||
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
|
||||
let color = if self.pk_colored {
|
||||
Some(pk_color(self.pk))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(profile) = self.profile {
|
||||
if let Some(prof) = profile.record().profile() {
|
||||
if prof.display_name().is_some() && prof.display_name().unwrap() != "" {
|
||||
ui_abbreviate_name(ui, prof.display_name().unwrap(), self.abbrev, color);
|
||||
} else if let Some(name) = prof.name() {
|
||||
ui_abbreviate_name(ui, name, self.abbrev, color);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut txt = RichText::new("nostrich").family(NamedFontFamily::Medium.as_family());
|
||||
if let Some(col) = color {
|
||||
txt = txt.color(col)
|
||||
}
|
||||
ui.label(txt);
|
||||
}
|
||||
})
|
||||
.response
|
||||
}
|
||||
}
|
||||
|
||||
fn colored_name(name: &str, color: Option<Color32>) -> RichText {
|
||||
let mut txt = RichText::new(name).family(NamedFontFamily::Medium.as_family());
|
||||
|
||||
if let Some(color) = color {
|
||||
txt = txt.color(color);
|
||||
}
|
||||
|
||||
txt
|
||||
}
|
||||
|
||||
fn ui_abbreviate_name(ui: &mut egui::Ui, name: &str, len: usize, color: Option<Color32>) {
|
||||
let should_abbrev = name.len() > len;
|
||||
let name = if should_abbrev {
|
||||
let closest = crate::abbrev::floor_char_boundary(name, len);
|
||||
&name[..closest]
|
||||
} else {
|
||||
name
|
||||
};
|
||||
|
||||
ui.label(colored_name(name, color));
|
||||
|
||||
if should_abbrev {
|
||||
ui.label(colored_name("..", color));
|
||||
}
|
||||
}
|
||||
|
||||
fn pk_color(pk: &[u8; 32]) -> Color32 {
|
||||
Color32::from_rgb(pk[8], pk[10], pk[12])
|
||||
}
|
||||
@@ -1,41 +1,6 @@
|
||||
use egui::{emath::GuiRounding, Button, Pos2, Stroke, Widget};
|
||||
use egui::{Button, Widget};
|
||||
use notedeck::NotedeckTextStyle;
|
||||
|
||||
use super::anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE};
|
||||
|
||||
pub fn x_button(rect: egui::Rect) -> impl egui::Widget {
|
||||
move |ui: &mut egui::Ui| -> egui::Response {
|
||||
let max_width = rect.width();
|
||||
let helper = AnimationHelper::new_from_rect(ui, "user_search_close", rect);
|
||||
|
||||
let fill_color = ui.visuals().text_color();
|
||||
|
||||
let radius = max_width / (2.0 * ICON_EXPANSION_MULTIPLE);
|
||||
|
||||
let painter = ui.painter();
|
||||
let ppp = ui.ctx().pixels_per_point();
|
||||
let nw_edge = helper
|
||||
.scale_pos_from_center(Pos2::new(-radius, radius))
|
||||
.round_to_pixel_center(ppp);
|
||||
let se_edge = helper
|
||||
.scale_pos_from_center(Pos2::new(radius, -radius))
|
||||
.round_to_pixel_center(ppp);
|
||||
let sw_edge = helper
|
||||
.scale_pos_from_center(Pos2::new(-radius, -radius))
|
||||
.round_to_pixel_center(ppp);
|
||||
let ne_edge = helper
|
||||
.scale_pos_from_center(Pos2::new(radius, radius))
|
||||
.round_to_pixel_center(ppp);
|
||||
|
||||
let line_width = helper.scale_1d_pos(2.0);
|
||||
|
||||
painter.line_segment([nw_edge, se_edge], Stroke::new(line_width, fill_color));
|
||||
painter.line_segment([ne_edge, sw_edge], Stroke::new(line_width, fill_color));
|
||||
|
||||
helper.take_animation_response()
|
||||
}
|
||||
}
|
||||
|
||||
/// Sized and styled to match the figma design
|
||||
pub fn styled_button(text: &str, fill_color: egui::Color32) -> impl Widget + '_ {
|
||||
move |ui: &mut egui::Ui| -> egui::Response {
|
||||
|
||||
Reference in New Issue
Block a user