417
crates/notedeck_columns/src/ui/note/custom_zap.rs
Normal file
417
crates/notedeck_columns/src/ui/note/custom_zap.rs
Normal file
@@ -0,0 +1,417 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use egui::{
|
||||
emath::GuiRounding, pos2, vec2, Color32, CornerRadius, FontId, Frame, Label, Layout, Slider,
|
||||
Stroke,
|
||||
};
|
||||
use enostr::Pubkey;
|
||||
use nostrdb::{Ndb, ProfileRecord, Transaction};
|
||||
use notedeck::{
|
||||
fonts::get_font_size, get_profile_url, name::get_display_name, Images, NotedeckTextStyle,
|
||||
};
|
||||
use notedeck_ui::{colors, profile::display_name_widget, AnimationHelper, ProfilePic};
|
||||
|
||||
use crate::ui::widgets::styled_button_toggleable;
|
||||
|
||||
pub struct CustomZapView<'a> {
|
||||
images: &'a mut Images,
|
||||
ndb: &'a Ndb,
|
||||
txn: &'a Transaction,
|
||||
target_pubkey: &'a Pubkey,
|
||||
default_msats: u64,
|
||||
}
|
||||
|
||||
#[allow(clippy::new_without_default)]
|
||||
impl<'a> CustomZapView<'a> {
|
||||
pub fn new(
|
||||
images: &'a mut Images,
|
||||
ndb: &'a Ndb,
|
||||
txn: &'a Transaction,
|
||||
target_pubkey: &'a Pubkey,
|
||||
default_msats: u64,
|
||||
) -> Self {
|
||||
Self {
|
||||
target_pubkey,
|
||||
images,
|
||||
ndb,
|
||||
txn,
|
||||
default_msats,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<u64> {
|
||||
egui::Frame::NONE
|
||||
.inner_margin(egui::Margin::same(8))
|
||||
.show(ui, |ui| self.ui_internal(ui))
|
||||
.inner
|
||||
}
|
||||
|
||||
fn ui_internal(&mut self, ui: &mut egui::Ui) -> Option<u64> {
|
||||
show_title(ui);
|
||||
|
||||
ui.add_space(16.0);
|
||||
|
||||
let profile = self
|
||||
.ndb
|
||||
.get_profile_by_pubkey(self.txn, self.target_pubkey.bytes())
|
||||
.ok();
|
||||
let profile = profile.as_ref();
|
||||
show_profile(ui, self.images, profile);
|
||||
|
||||
ui.add_space(8.0);
|
||||
|
||||
let slider_width = {
|
||||
let desired_slider_width = ui.available_width() * 0.6;
|
||||
if desired_slider_width < 224.0 {
|
||||
224.0
|
||||
} else {
|
||||
desired_slider_width
|
||||
}
|
||||
};
|
||||
|
||||
let id = ui.id().with(("CustomZap", self.target_pubkey));
|
||||
|
||||
let default_sats = self.default_msats / 1000;
|
||||
ui.with_layout(Layout::top_down(egui::Align::Center), |ui| {
|
||||
ui.spacing_mut().item_spacing = vec2(0.0, 16.0);
|
||||
ui.spacing_mut().slider_width = slider_width;
|
||||
|
||||
let mut cur_amount = if let Some(input) = ui.data(|d| d.get_temp(id)) {
|
||||
input
|
||||
} else {
|
||||
(self.default_msats / 1000).to_string()
|
||||
};
|
||||
show_amount(ui, id, &mut cur_amount, slider_width);
|
||||
let mut maybe_sats = cur_amount.parse::<u64>().ok();
|
||||
|
||||
let prev_slider_sats = maybe_sats.unwrap_or(default_sats).clamp(1, 100000);
|
||||
let mut slider_sats = prev_slider_sats;
|
||||
ui.allocate_new_ui(egui::UiBuilder::new(), |ui| {
|
||||
ui.set_width(slider_width);
|
||||
ui.add(
|
||||
Slider::new(&mut slider_sats, 1..=100000)
|
||||
.logarithmic(true)
|
||||
.trailing_fill(true)
|
||||
.show_value(false),
|
||||
);
|
||||
});
|
||||
|
||||
if slider_sats != prev_slider_sats {
|
||||
cur_amount = slider_sats.to_string();
|
||||
maybe_sats = Some(slider_sats);
|
||||
}
|
||||
|
||||
if let Some(selection) = show_selection_buttons(ui, maybe_sats) {
|
||||
cur_amount = selection.to_string();
|
||||
maybe_sats = Some(selection);
|
||||
}
|
||||
|
||||
ui.data_mut(|d| d.insert_temp(id, cur_amount));
|
||||
|
||||
let resp = ui.add(styled_button_toggleable(
|
||||
"Send",
|
||||
colors::PINK,
|
||||
is_valid_zap(maybe_sats),
|
||||
));
|
||||
|
||||
if resp.clicked() {
|
||||
maybe_sats.map(|i| i * 1000)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.inner
|
||||
}
|
||||
}
|
||||
|
||||
fn is_valid_zap(amount: Option<u64>) -> bool {
|
||||
amount.map_or(false, |sats| sats > 0)
|
||||
}
|
||||
|
||||
fn show_title(ui: &mut egui::Ui) {
|
||||
let max_size = 32.0;
|
||||
ui.allocate_ui_with_layout(
|
||||
vec2(ui.available_width(), max_size),
|
||||
Layout::left_to_right(egui::Align::Center),
|
||||
|ui| {
|
||||
let (rect, _) = ui.allocate_exact_size(vec2(max_size, max_size), egui::Sense::hover());
|
||||
let painter = ui.painter_at(rect);
|
||||
let circle_color = lerp_color(
|
||||
egui::Color32::from_rgb(0xFF, 0xB7, 0x57),
|
||||
ui.visuals().noninteractive().bg_fill,
|
||||
0.5,
|
||||
);
|
||||
painter.circle_filled(rect.center(), max_size / 2.0, circle_color);
|
||||
|
||||
let img_data = egui::include_image!("../../../../../assets/icons/filled_zap_icon.svg");
|
||||
let zap_max_width = 25.16;
|
||||
let zap_max_height = 29.34;
|
||||
let img = egui::Image::new(img_data)
|
||||
.max_width(zap_max_width)
|
||||
.max_height(zap_max_height);
|
||||
|
||||
let img_rect = rect
|
||||
.shrink2(vec2(max_size - zap_max_width, max_size - zap_max_height))
|
||||
.round_to_pixel_center(ui.pixels_per_point());
|
||||
img.paint_at(ui, img_rect);
|
||||
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.add(egui::Label::new(
|
||||
egui::RichText::new("Zap").text_style(NotedeckTextStyle::Heading2.text_style()),
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn show_profile(ui: &mut egui::Ui, images: &mut Images, profile: Option<&ProfileRecord>) {
|
||||
let max_size = 24.0;
|
||||
ui.allocate_ui_with_layout(
|
||||
vec2(ui.available_width(), max_size),
|
||||
Layout::left_to_right(egui::Align::Center).with_main_wrap(true),
|
||||
|ui| {
|
||||
ui.add(&mut ProfilePic::new(images, get_profile_url(profile)).size(max_size));
|
||||
ui.add(display_name_widget(&get_display_name(profile), false));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn show_amount(ui: &mut egui::Ui, id: egui::Id, user_input: &mut String, width: f32) {
|
||||
let user_input_font = NotedeckTextStyle::Heading.get_bolded_font(ui.ctx());
|
||||
|
||||
let user_input_id = id.with("sats_amount");
|
||||
|
||||
let user_input_galley = ui.painter().layout_no_wrap(
|
||||
user_input.to_owned(),
|
||||
user_input_font.clone(),
|
||||
ui.visuals().text_color(),
|
||||
);
|
||||
|
||||
let painter = ui.painter();
|
||||
|
||||
let sats_galley = painter.layout_no_wrap(
|
||||
"SATS".to_owned(),
|
||||
NotedeckTextStyle::Heading4.get_font_id(ui.ctx()),
|
||||
ui.visuals().noninteractive().text_color(),
|
||||
);
|
||||
|
||||
let user_input_rect = {
|
||||
let mut rect = user_input_galley.rect;
|
||||
rect.extend_with_x(user_input_galley.rect.left() - 8.0);
|
||||
rect
|
||||
};
|
||||
let sats_width = sats_galley.rect.width() + 8.0;
|
||||
|
||||
Frame::NONE
|
||||
.fill(ui.visuals().noninteractive().weak_bg_fill)
|
||||
.corner_radius(8)
|
||||
.show(ui, |ui| {
|
||||
ui.set_width(width);
|
||||
ui.add_space(8.0);
|
||||
ui.with_layout(Layout::top_down(egui::Align::Center), |ui| {
|
||||
let textedit = egui::TextEdit::singleline(user_input)
|
||||
.frame(false)
|
||||
.id(user_input_id)
|
||||
.font(user_input_font);
|
||||
|
||||
let amount_resp = ui.add(Label::new(
|
||||
egui::RichText::new("Amount")
|
||||
.text_style(NotedeckTextStyle::Heading3.text_style())
|
||||
.color(ui.visuals().noninteractive().text_color()),
|
||||
));
|
||||
|
||||
let user_input_padding = {
|
||||
let available_width = ui.available_width();
|
||||
if user_input_rect.width() + sats_width > available_width {
|
||||
0.0
|
||||
} else if (user_input_rect.width() / 2.0) + sats_width > (available_width / 2.0)
|
||||
{
|
||||
available_width - sats_width - user_input_rect.width()
|
||||
} else {
|
||||
(available_width / 2.0) - (user_input_rect.width() / 2.0)
|
||||
}
|
||||
};
|
||||
|
||||
let user_input_rect = {
|
||||
let max_input_width = ui.available_width() - sats_width;
|
||||
|
||||
let user_input_size = if user_input_rect.width() > max_input_width {
|
||||
vec2(max_input_width, user_input_rect.height())
|
||||
} else {
|
||||
user_input_rect.size()
|
||||
};
|
||||
|
||||
let user_input_pos = pos2(
|
||||
ui.available_rect_before_wrap().left() + user_input_padding,
|
||||
amount_resp.rect.bottom(),
|
||||
);
|
||||
egui::Rect::from_min_size(user_input_pos, user_input_size)
|
||||
.intersect(ui.available_rect_before_wrap())
|
||||
};
|
||||
|
||||
let textout = ui
|
||||
.allocate_new_ui(
|
||||
egui::UiBuilder::new()
|
||||
.max_rect(user_input_rect)
|
||||
.layout(Layout::centered_and_justified(egui::Direction::TopDown)),
|
||||
|ui| textedit.show(ui),
|
||||
)
|
||||
.inner;
|
||||
|
||||
let out_rect = textout.text_clip_rect;
|
||||
|
||||
ui.advance_cursor_after_rect(out_rect);
|
||||
|
||||
let sats_pos = pos2(
|
||||
out_rect.right() + 8.0,
|
||||
out_rect.center().y - (sats_galley.rect.height() / 2.0),
|
||||
);
|
||||
|
||||
let sats_rect = egui::Rect::from_min_size(sats_pos, sats_galley.size());
|
||||
ui.painter()
|
||||
.galley(sats_pos, sats_galley, ui.visuals().text_color());
|
||||
|
||||
ui.advance_cursor_after_rect(sats_rect);
|
||||
|
||||
if !is_valid_zap(user_input.parse::<u64>().ok()) {
|
||||
ui.colored_label(ui.visuals().warn_fg_color, "Please enter valid amount.");
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
});
|
||||
});
|
||||
|
||||
// let user_changed = cur_input != Some(user_input.clone());
|
||||
ui.memory_mut(|m| m.request_focus(user_input_id));
|
||||
// ui.data_mut(|d| d.insert_temp(id, user_input));
|
||||
}
|
||||
|
||||
const SELECTION_BUTTONS: [ZapSelectionButton; 8] = [
|
||||
ZapSelectionButton::First,
|
||||
ZapSelectionButton::Second,
|
||||
ZapSelectionButton::Third,
|
||||
ZapSelectionButton::Fourth,
|
||||
ZapSelectionButton::Fifth,
|
||||
ZapSelectionButton::Sixth,
|
||||
ZapSelectionButton::Seventh,
|
||||
ZapSelectionButton::Eighth,
|
||||
];
|
||||
|
||||
fn show_selection_buttons(ui: &mut egui::Ui, sats_selection: Option<u64>) -> Option<u64> {
|
||||
let mut our_selection = None;
|
||||
ui.allocate_ui_with_layout(
|
||||
vec2(224.0, 116.0),
|
||||
Layout::left_to_right(egui::Align::Min).with_main_wrap(true),
|
||||
|ui| {
|
||||
ui.spacing_mut().item_spacing = vec2(8.0, 8.0);
|
||||
|
||||
for button in SELECTION_BUTTONS {
|
||||
our_selection = our_selection.or(show_selection_button(ui, sats_selection, button));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
our_selection
|
||||
}
|
||||
|
||||
fn show_selection_button(
|
||||
ui: &mut egui::Ui,
|
||||
sats_selection: Option<u64>,
|
||||
button: ZapSelectionButton,
|
||||
) -> Option<u64> {
|
||||
let (rect, _) = ui.allocate_exact_size(vec2(50.0, 50.0), egui::Sense::click());
|
||||
let helper = AnimationHelper::new_from_rect(ui, ("zap_selection_button", &button), rect);
|
||||
let painter = ui.painter();
|
||||
|
||||
let corner = CornerRadius::same(8);
|
||||
painter.rect_filled(rect, corner, ui.visuals().noninteractive().weak_bg_fill);
|
||||
|
||||
let amount = button.sats();
|
||||
let current_selected = if let Some(selection) = sats_selection {
|
||||
selection == amount
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if current_selected {
|
||||
painter.rect_stroke(
|
||||
rect,
|
||||
corner,
|
||||
Stroke {
|
||||
width: 1.0,
|
||||
color: colors::PINK,
|
||||
},
|
||||
egui::StrokeKind::Inside,
|
||||
);
|
||||
}
|
||||
|
||||
let fontid = FontId::new(
|
||||
helper.scale_1d_pos(get_font_size(ui.ctx(), &NotedeckTextStyle::Body)),
|
||||
NotedeckTextStyle::Body.font_family(),
|
||||
);
|
||||
|
||||
let galley = painter.layout_no_wrap(button.to_string(), fontid, ui.visuals().text_color());
|
||||
let text_rect = {
|
||||
let mut galley_rect = galley.rect;
|
||||
galley_rect.set_center(rect.center());
|
||||
galley_rect
|
||||
};
|
||||
|
||||
painter.galley(text_rect.min, galley, ui.visuals().text_color());
|
||||
|
||||
if helper.take_animation_response().clicked() {
|
||||
return Some(amount);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[derive(Hash)]
|
||||
enum ZapSelectionButton {
|
||||
First,
|
||||
Second,
|
||||
Third,
|
||||
Fourth,
|
||||
Fifth,
|
||||
Sixth,
|
||||
Seventh,
|
||||
Eighth,
|
||||
}
|
||||
|
||||
impl ZapSelectionButton {
|
||||
pub fn sats(&self) -> u64 {
|
||||
match self {
|
||||
ZapSelectionButton::First => 69,
|
||||
ZapSelectionButton::Second => 100,
|
||||
ZapSelectionButton::Third => 420,
|
||||
ZapSelectionButton::Fourth => 5_000,
|
||||
ZapSelectionButton::Fifth => 10_000,
|
||||
ZapSelectionButton::Sixth => 20_000,
|
||||
ZapSelectionButton::Seventh => 50_000,
|
||||
ZapSelectionButton::Eighth => 100_000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ZapSelectionButton {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ZapSelectionButton::First => write!(f, "69"),
|
||||
ZapSelectionButton::Second => write!(f, "100"),
|
||||
ZapSelectionButton::Third => write!(f, "420"),
|
||||
ZapSelectionButton::Fourth => write!(f, "5K"),
|
||||
ZapSelectionButton::Fifth => write!(f, "10K"),
|
||||
ZapSelectionButton::Sixth => write!(f, "20K"),
|
||||
ZapSelectionButton::Seventh => write!(f, "50K"),
|
||||
ZapSelectionButton::Eighth => write!(f, "100K"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn lerp_color(a: Color32, b: Color32, t: f32) -> Color32 {
|
||||
Color32::from_rgba_premultiplied(
|
||||
egui::lerp(a.r() as f32..=b.r() as f32, t) as u8,
|
||||
egui::lerp(a.g() as f32..=b.g() as f32, t) as u8,
|
||||
egui::lerp(a.b() as f32..=b.b() as f32, t) as u8,
|
||||
egui::lerp(a.a() as f32..=b.a() as f32, t) as u8,
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod custom_zap;
|
||||
pub mod post;
|
||||
pub mod quote_repost;
|
||||
pub mod reply;
|
||||
|
||||
Reference in New Issue
Block a user