refactor data completion, add initial design from karnage

This commit is contained in:
William Casarin 2023-12-20 08:36:28 -08:00
parent 7a41982f14
commit 6cc648652a
6 changed files with 640 additions and 93 deletions

View File

@ -7,20 +7,65 @@ use tokio::sync::broadcast::error::RecvError;
pub enum Error {
Nip19(nip19::Error),
Http(hyper::http::Error),
Hyper(hyper::Error),
Nostrdb(nostrdb::Error),
NostrClient(nostr_sdk::client::Error),
Recv(RecvError),
Io(std::io::Error),
Generic(String),
Image(image::error::ImageError),
Secp(nostr_sdk::secp256k1::Error),
InvalidUri,
NotFound,
/// Profile picture is too big
TooBig,
InvalidNip19,
InvalidProfilePic,
SliceErr,
}
impl From<image::error::ImageError> for Error {
fn from(err: image::error::ImageError) -> Self {
Error::Image(err)
}
}
impl From<http::uri::InvalidUri> for Error {
fn from(err: http::uri::InvalidUri) -> Self {
Error::InvalidUri
}
}
impl From<nostr_sdk::secp256k1::Error> for Error {
fn from(err: nostr_sdk::secp256k1::Error) -> Self {
Error::Secp(err)
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self {
Error::Io(err)
}
}
impl From<String> for Error {
fn from(err: String) -> Self {
Error::Generic(err)
}
}
impl From<RecvError> for Error {
fn from(err: RecvError) -> Self {
Error::Recv(err)
}
}
impl From<hyper::Error> for Error {
fn from(err: hyper::Error) -> Self {
Error::Hyper(err)
}
}
impl From<TryFromSliceError> for Error {
fn from(_: TryFromSliceError) -> Self {
Error::SliceErr
@ -63,6 +108,14 @@ impl fmt::Display for Error {
Error::Recv(e) => write!(f, "Recieve error: {}", e),
Error::InvalidNip19 => write!(f, "Invalid nip19 object"),
Error::SliceErr => write!(f, "Array slice error"),
Error::TooBig => write!(f, "Profile picture is too big"),
Error::InvalidProfilePic => write!(f, "Profile picture is corrupt"),
Error::Image(err) => write!(f, "Image error: {}", err),
Error::InvalidUri => write!(f, "Invalid url"),
Error::Hyper(err) => write!(f, "Hyper error: {}", err),
Error::Generic(err) => write!(f, "Generic error: {}", err),
Error::Io(err) => write!(f, "Io error: {}", err),
Error::Secp(err) => write!(f, "Signature error: {}", err),
}
}
}

View File

@ -1,7 +1,6 @@
// TODO: figure out the custom font situation
/*
fn setup_fonts(font_data: &egui::FontData, ctx: &egui::Context) {
pub fn setup_fonts(font_data: &egui::FontData, ctx: &egui::Context) {
let mut fonts = egui::FontDefinitions::default();
// Install my own font (maybe supporting non-latin characters).
@ -20,4 +19,3 @@ fn setup_fonts(font_data: &egui::FontData, ctx: &egui::Context) {
// Tell egui to use these fonts:
ctx.set_fonts(fonts);
}
*/

View File

@ -12,6 +12,7 @@ use std::sync::Arc;
use tokio::net::TcpListener;
use crate::error::Error;
use crate::render::NoteRenderData;
use nostr_sdk::prelude::*;
use nostrdb::{Config, Ndb, Transaction};
use std::time::Duration;
@ -19,28 +20,32 @@ use std::time::Duration;
use lru::LruCache;
mod error;
mod fonts;
mod gradient;
mod nip19;
mod pfp;
mod render;
pub enum Target {
Profile(XOnlyPublicKey),
Event(EventId),
}
type ImageCache = LruCache<XOnlyPublicKey, egui::TextureHandle>;
#[derive(Debug, Clone)]
#[derive(Clone)]
pub struct Notecrumbs {
ndb: Ndb,
keys: Keys,
font_data: egui::FontData,
img_cache: Arc<ImageCache>,
default_pfp: egui::ImageData,
/// How long do we wait for remote note requests
timeout: Duration,
}
async fn find_note(app: &Notecrumbs, nip19: &Nip19) -> Result<nostr_sdk::Event, Error> {
pub struct FindNoteResult {
note: Option<Event>,
profile: Option<Event>,
}
pub async fn find_note(app: &Notecrumbs, nip19: &Nip19) -> Result<FindNoteResult, Error> {
let opts = Options::new().shutdown_on_drop(true);
let client = Client::with_opts(&app.keys, opts);
@ -59,16 +64,28 @@ async fn find_note(app: &Notecrumbs, nip19: &Nip19) -> Result<nostr_sdk::Event,
.req_events_of(filters.clone(), Some(app.timeout))
.await;
let mut note: Option<Event> = None;
let mut profile: Option<Event> = None;
loop {
match client.notifications().recv().await? {
RelayPoolNotification::Event(_url, ev) => {
info!("got ev: {:?}", ev);
return Ok(ev);
debug!("got event 1 {:?}", ev);
note = Some(ev);
return Ok(FindNoteResult { note, profile });
}
RelayPoolNotification::RelayStatus { .. } => continue,
RelayPoolNotification::Message(_url, msg) => match msg {
RelayMessage::Event { event, .. } => return Ok(*event),
RelayMessage::EndOfStoredEvents(_) => return Err(Error::NotFound),
RelayMessage::Event { event, .. } => {
if event.kind == Kind::Metadata {
debug!("got profile {:?}", event);
profile = Some(*event);
} else {
debug!("got event {:?}", event);
note = Some(*event);
}
}
RelayMessage::EndOfStoredEvents(_) => return Ok(FindNoteResult { note, profile }),
_ => continue,
},
RelayPoolNotification::Stop | RelayPoolNotification::Shutdown => {
@ -91,66 +108,22 @@ async fn serve(
}
};
let target = match nip19::to_target(&nip19) {
Some(target) => target,
None => {
return Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Full::new(Bytes::from("\n")))?)
}
};
let content = {
let mut txn = Transaction::new(&app.ndb)?;
match target {
Target::Profile(pk) => app
.ndb
.get_profile_by_pubkey(&mut txn, &pk.serialize())
.and_then(|n| {
info!("profile cache hit {:?}", nip19);
Ok(n.record
.profile()
.ok_or(nostrdb::Error::NotFound)?
.name()
.ok_or(nostrdb::Error::NotFound)?
.to_string())
}),
Target::Event(evid) => app
.ndb
.get_note_by_id(&mut txn, evid.as_bytes().try_into()?)
.map(|n| {
info!("event cache hit {:?}", nip19);
n.content().to_string()
}),
}
};
let content = match content {
Ok(content) => content,
Err(nostrdb::Error::NotFound) => {
debug!("Finding {:?}", nip19);
match find_note(app, &nip19).await {
Ok(note) => {
let _ = app
.ndb
.process_event(&json!(["EVENT", "s", note]).to_string());
note.content
}
Err(_err) => {
return Ok(Response::builder().status(StatusCode::NOT_FOUND).body(
Full::new(Bytes::from(format!("noteid {:?} not found\n", nip19))),
)?);
}
}
}
// render_data is always returned, it just might be empty
let partial_render_data = match render::get_render_data(&app, &nip19) {
Err(err) => {
return Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Full::new(Bytes::from(format!("{}\n", err))))?);
.status(StatusCode::BAD_REQUEST)
.body(Full::new(Bytes::from(
"nsecs are not supported, what were you thinking!?\n",
)))?);
}
Ok(render_data) => render_data,
};
let data = render::render_note(&app, &content);
// fetch extra data if we are missing it
let render_data = partial_render_data.complete(&app, &nip19).await;
let data = render::render_note(&app, &render_data);
Ok(Response::builder()
.header(header::CONTENT_TYPE, "image/png")
@ -164,6 +137,33 @@ fn get_env_timeout() -> Duration {
Duration::from_millis(timeout_ms)
}
fn get_gradient() -> egui::ColorImage {
use egui::{pos2, Color32, ColorImage};
use gradient::Gradient;
//let gradient = Gradient::linear(Color32::LIGHT_GRAY, Color32::DARK_GRAY);
let size = pfp::PFP_SIZE as usize;
let radius = (pfp::PFP_SIZE as f32) / 2.0;
let center = pos2(radius, radius);
let start_color = Color32::from_rgb(0x1E, 0x55, 0xFF);
let end_color = Color32::from_rgb(0xFA, 0x0D, 0xD4);
let gradient = Gradient::radial_alpha_gradient(center, radius, start_color, end_color);
let pixels = gradient.to_pixel_row();
assert_eq!(pixels.len(), size * size);
ColorImage {
size: [size, size],
pixels,
}
}
fn get_default_pfp() -> egui::ColorImage {
let img = std::fs::read("assets/default_pfp_2.png").expect("default pfp missing");
let mut dyn_image = image::load_from_memory(&img).expect("failed to load default pfp");
pfp::process_pfp_bitmap(&mut dyn_image)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
env_logger::init();
@ -181,11 +181,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let keys = Keys::generate();
let timeout = get_env_timeout();
let img_cache = Arc::new(LruCache::new(std::num::NonZeroUsize::new(64).unwrap()));
let default_pfp = egui::ImageData::Color(Arc::new(get_default_pfp()));
//let default_pfp = egui::ImageData::Color(get_gradient());
let font_data = egui::FontData::from_static(include_bytes!("../fonts/NotoSans-Regular.ttf"));
let app = Notecrumbs {
ndb,
keys,
timeout,
img_cache,
font_data,
default_pfp,
};
// We start a loop to continuously accept incoming connections

View File

@ -3,16 +3,6 @@ use crate::Target;
use nostr_sdk::nips::nip19::Nip19;
use nostr_sdk::prelude::*;
pub fn to_target(nip19: &Nip19) -> Option<Target> {
match nip19 {
Nip19::Event(ev) => Some(Target::Event(ev.event_id)),
Nip19::EventId(evid) => Some(Target::Event(*evid)),
Nip19::Profile(prof) => Some(Target::Profile(prof.public_key)),
Nip19::Pubkey(pk) => Some(Target::Profile(*pk)),
Nip19::Secret(_) => None,
}
}
pub fn to_filters(nip19: &Nip19) -> Result<Vec<Filter>, Error> {
match nip19 {
Nip19::Event(ev) => {

View File

@ -1,6 +1,11 @@
use crate::Error;
use bytes::Bytes;
use egui::{Color32, ColorImage};
use hyper::body::Incoming;
use image::imageops::FilterType;
pub const PFP_SIZE: u32 = 64;
// Thank to gossip for this one!
pub fn round_image(image: &mut ColorImage) {
#[cfg(feature = "profiling")]
@ -49,10 +54,12 @@ pub fn round_image(image: &mut ColorImage) {
}
}
fn process_pfp_bitmap(size: u32, image: &mut image::DynamicImage) -> ColorImage {
pub fn process_pfp_bitmap(image: &mut image::DynamicImage) -> ColorImage {
#[cfg(features = "profiling")]
puffin::profile_function!();
let size = PFP_SIZE;
// Crop square
let smaller = image.width().min(image.height());
@ -75,3 +82,91 @@ fn process_pfp_bitmap(size: u32, image: &mut image::DynamicImage) -> ColorImage
round_image(&mut color_image);
color_image
}
async fn fetch_url(url: &str) -> Result<(Vec<u8>, hyper::Response<Incoming>), Error> {
use http_body_util::BodyExt;
use http_body_util::Empty;
use hyper::Request;
use hyper_util::rt::tokio::TokioIo;
use tokio::net::TcpStream;
let mut data: Vec<u8> = vec![];
let url = url.parse::<hyper::Uri>()?;
let host = url.host().expect("uri has no host");
let port = url.port_u16().unwrap_or(80);
let addr = format!("{}:{}", host, port);
let stream = TcpStream::connect(addr).await?;
let io = TokioIo::new(stream);
let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await?;
tokio::task::spawn(async move {
if let Err(err) = conn.await {
println!("Connection failed: {:?}", err);
}
});
let authority = url.authority().unwrap().clone();
let req = Request::builder()
.uri(url)
.header(hyper::header::HOST, authority.as_str())
.body(Empty::<Bytes>::new())?;
let mut res: hyper::Response<Incoming> = sender.send_request(req).await?;
// Stream the body, writing each chunk to stdout as we get it
// (instead of buffering and printing at the end).
while let Some(next) = res.frame().await {
let frame = next?;
if let Some(chunk) = frame.data_ref() {
if data.len() + chunk.len() > 52428800
/* 50 MiB */
{
return Err(Error::TooBig);
}
data.extend(chunk);
}
}
Ok((data, res))
}
pub async fn fetch_pfp(url: &str) -> Result<ColorImage, Error> {
let (data, res) = fetch_url(url).await?;
parse_img_response(data, res)
}
fn parse_img_response(
data: Vec<u8>,
response: hyper::Response<Incoming>,
) -> Result<ColorImage, Error> {
use egui_extras::image::FitTo;
#[cfg(feature = "profiling")]
puffin::profile_function!();
let content_type = response.headers()["content-type"]
.to_str()
.unwrap_or_default();
let size = PFP_SIZE;
if content_type.starts_with("image/svg") {
#[cfg(feature = "profiling")]
puffin::profile_scope!("load_svg");
let mut color_image = egui_extras::image::load_svg_bytes_with_size(
&data,
FitTo::Size(size as u32, size as u32),
)?;
round_image(&mut color_image);
Ok(color_image)
} else if content_type.starts_with("image/") {
#[cfg(feature = "profiling")]
puffin::profile_scope!("load_from_memory");
let mut dyn_image = image::load_from_memory(&data)?;
Ok(process_pfp_bitmap(&mut dyn_image))
} else {
Err(Error::InvalidProfilePic)
}
}

View File

@ -1,30 +1,423 @@
struct ProfileRenderData {}
use crate::{fonts, Error, Notecrumbs};
use egui::{Color32, ColorImage, FontId, RichText, Visuals};
use log::{debug, info, warn};
use nostr_sdk::nips::nip19::Nip19;
use nostr_sdk::prelude::*;
use nostrdb::{Note, Transaction};
use std::sync::Arc;
use crate::Notecrumbs;
impl ProfileRenderData {
pub fn default(pfp: egui::ImageData) -> Self {
ProfileRenderData {
name: "nostrich".to_string(),
display_name: None,
about: "A am a nosy nostrich".to_string(),
pfp: pfp,
}
}
}
struct NoteRenderData {
#[derive(Debug, Clone)]
pub struct NoteData {
content: String,
}
pub struct ProfileRenderData {
name: String,
display_name: Option<String>,
about: String,
pfp: egui::ImageData,
}
pub struct NoteRenderData {
note: NoteData,
profile: ProfileRenderData,
}
enum RenderData {
Note(NoteRenderData),
pub struct PartialNoteRenderData {
note: Option<NoteData>,
profile: Option<ProfileRenderData>,
}
fn note_ui(app: &Notecrumbs, ctx: &egui::Context, content: &str) {
pub enum PartialRenderData {
Note(PartialNoteRenderData),
Profile(Option<ProfileRenderData>),
}
pub enum RenderData {
Note(NoteRenderData),
Profile(ProfileRenderData),
}
#[derive(Debug)]
pub enum EventSource {
Nip19(Nip19Event),
Id(EventId),
}
impl EventSource {
fn id(&self) -> EventId {
match self {
EventSource::Nip19(ev) => ev.event_id,
EventSource::Id(id) => *id,
}
}
fn author(&self) -> Option<XOnlyPublicKey> {
match self {
EventSource::Nip19(ev) => ev.author,
EventSource::Id(_) => None,
}
}
}
impl From<Nip19Event> for EventSource {
fn from(event: Nip19Event) -> EventSource {
EventSource::Nip19(event)
}
}
impl From<EventId> for EventSource {
fn from(event_id: EventId) -> EventSource {
EventSource::Id(event_id)
}
}
impl NoteData {
fn default() -> Self {
let content = "".to_string();
NoteData { content }
}
}
impl PartialRenderData {
pub async fn complete(self, app: &Notecrumbs, nip19: &Nip19) -> RenderData {
match self {
PartialRenderData::Note(partial) => {
RenderData::Note(partial.complete(app, nip19).await)
}
PartialRenderData::Profile(Some(profile)) => RenderData::Profile(profile),
PartialRenderData::Profile(None) => {
warn!("TODO: implement profile data completion");
RenderData::Profile(ProfileRenderData::default(app.default_pfp.clone()))
}
}
}
}
impl PartialNoteRenderData {
pub async fn complete(self, app: &Notecrumbs, nip19: &Nip19) -> NoteRenderData {
// we have everything, all done!
match (self.note, self.profile) {
(Some(note), Some(profile)) => {
return NoteRenderData { note, profile };
}
// Don't hold ourselves up on profile data for notes. We can spin
// off a background task to find the profile though.
(Some(note), None) => {
warn!("TODO: spin off profile query when missing note profile");
let profile = ProfileRenderData::default(app.default_pfp.clone());
return NoteRenderData { note, profile };
}
_ => (),
}
debug!("Finding {:?}", nip19);
match crate::find_note(app, &nip19).await {
Ok(note_res) => {
let note = match note_res.note {
Some(note) => {
debug!("saving {:?} to nostrdb", &note);
let _ = app
.ndb
.process_event(&json!(["EVENT", "s", note]).to_string());
sdk_note_to_note_data(&note)
}
None => NoteData::default(),
};
let profile = match note_res.profile {
Some(profile) => {
debug!("saving profile to nostrdb: {:?}", &profile);
let _ = app
.ndb
.process_event(&json!(["EVENT", "s", profile]).to_string());
// TODO: wire profile to profile data, download pfp
ProfileRenderData::default(app.default_pfp.clone())
}
None => ProfileRenderData::default(app.default_pfp.clone()),
};
NoteRenderData { note, profile }
}
Err(_err) => {
let note = NoteData::default();
let profile = ProfileRenderData::default(app.default_pfp.clone());
NoteRenderData { note, profile }
}
}
}
}
fn get_profile_render_data(
txn: &Transaction,
app: &Notecrumbs,
pubkey: &XOnlyPublicKey,
) -> Result<ProfileRenderData, Error> {
let profile = app.ndb.get_profile_by_pubkey(&txn, &pubkey.serialize())?;
info!("profile cache hit {:?}", pubkey);
let profile = profile.record.profile().ok_or(nostrdb::Error::NotFound)?;
let name = profile.name().unwrap_or("").to_string();
let about = profile.about().unwrap_or("").to_string();
let display_name = profile.display_name().as_ref().map(|a| a.to_string());
let pfp = app.default_pfp.clone();
Ok(ProfileRenderData {
name,
pfp,
about,
display_name,
})
}
fn ndb_note_to_data(note: &Note) -> NoteData {
let content = note.content().to_string();
NoteData { content }
}
fn sdk_note_to_note_data(note: &Event) -> NoteData {
let content = note.content.clone();
NoteData { content }
}
fn get_note_render_data(
app: &Notecrumbs,
source: &EventSource,
) -> Result<PartialNoteRenderData, Error> {
debug!("got here a");
let txn = Transaction::new(&app.ndb)?;
let m_note = app
.ndb
.get_note_by_id(&txn, source.id().as_bytes().try_into()?)
.map_err(Error::Nostrdb);
debug!("note cached? {:?}", m_note);
// It's possible we have an author pk in an nevent, let's use it if we do.
// This gives us the opportunity to load the profile picture earlier if we
// have a cached profile
let mut profile: Option<ProfileRenderData> = None;
let m_note_pk = m_note
.as_ref()
.ok()
.and_then(|n| XOnlyPublicKey::from_slice(n.pubkey()).ok());
let m_pk = m_note_pk.or(source.author());
// get profile render data if we can
if let Some(pk) = m_pk {
match get_profile_render_data(&txn, app, &pk) {
Err(err) => warn!(
"No profile found for {} for note {}: {}",
&pk,
&source.id(),
err
),
Ok(record) => {
debug!("profile record found for note");
profile = Some(record);
}
}
}
let note = m_note.map(|n| ndb_note_to_data(&n)).ok();
Ok(PartialNoteRenderData { profile, note })
}
pub fn get_render_data(app: &Notecrumbs, target: &Nip19) -> Result<PartialRenderData, Error> {
match target {
Nip19::Profile(profile) => {
let txn = Transaction::new(&app.ndb)?;
Ok(PartialRenderData::Profile(
get_profile_render_data(&txn, app, &profile.public_key).ok(),
))
}
Nip19::Pubkey(pk) => {
let txn = Transaction::new(&app.ndb)?;
Ok(PartialRenderData::Profile(
get_profile_render_data(&txn, app, pk).ok(),
))
}
Nip19::Event(event) => Ok(PartialRenderData::Note(get_note_render_data(
app,
&EventSource::Nip19(event.clone()),
)?)),
Nip19::EventId(evid) => Ok(PartialRenderData::Note(get_note_render_data(
app,
&EventSource::Id(*evid),
)?)),
Nip19::Secret(_nsec) => Err(Error::InvalidNip19),
}
}
#[inline]
pub fn floor_char_boundary(s: &str, index: usize) -> usize {
if index >= s.len() {
s.len()
} else {
let lower_bound = index.saturating_sub(3);
let new_index = s.as_bytes()[lower_bound..=index]
.iter()
.rposition(|b| is_utf8_char_boundary(*b));
// SAFETY: we know that the character boundary will be within four bytes
unsafe { lower_bound + new_index.unwrap_unchecked() }
}
}
#[inline]
fn is_utf8_char_boundary(c: u8) -> bool {
// This is bit magic equivalent to: b < 128 || b >= 192
(c as i8) >= -0x40
}
fn ui_abbreviate_name(ui: &mut egui::Ui, name: &str, len: usize) {
if name.len() > len {
let closest = floor_char_boundary(name, len);
heading(ui, &name[..closest]);
heading(ui, "...");
} else {
heading(ui, name);
}
}
fn render_username(app: &Notecrumbs, ui: &mut egui::Ui, profile: &ProfileRenderData) {
#[cfg(feature = "profiling")]
puffin::profile_function!();
let name = format!("@{}", profile.name);
ui.label(RichText::new(&name).size(30.0).color(Color32::DARK_GRAY));
}
fn heading(ui: &mut egui::Ui, text: impl Into<RichText>) {
ui.label(text.into().size(40.0));
}
fn setup_visuals(font_data: &egui::FontData, ctx: &egui::Context) {
let mut visuals = Visuals::dark();
visuals.override_text_color = Some(Color32::WHITE);
ctx.set_visuals(visuals);
fonts::setup_fonts(font_data, ctx);
}
fn wrapped_body(ui: &mut egui::Ui, text: &str) {
use egui::text::{LayoutJob, TextFormat};
let format = TextFormat {
font_id: FontId::proportional(40.0),
color: Color32::WHITE,
extra_letter_spacing: -1.0,
line_height: Some(40.0),
..Default::default()
};
let mut job = LayoutJob::single_section(text.to_owned(), format);
job.justify = false;
job.halign = egui::Align::LEFT;
job.wrap = egui::text::TextWrapping {
max_rows: 5,
break_anywhere: false,
overflow_character: Some('…'),
..Default::default()
};
ui.label(job);
}
fn centered_layout() -> egui::Layout {
use egui::{Align, Direction, Layout};
Layout {
main_dir: Direction::TopDown,
main_wrap: true,
main_align: Align::Center,
main_justify: true,
cross_align: Align::Center,
cross_justify: true,
}
}
fn note_ui(app: &Notecrumbs, ctx: &egui::Context, note: &NoteRenderData) {
use egui::{FontId, Label, RichText, Rounding};
let pfp = ctx.load_texture("pfp", note.profile.pfp.clone(), Default::default());
setup_visuals(&app.font_data, ctx);
let outer_margin = 40.0;
let inner_margin = 100.0;
let total_margin = outer_margin + inner_margin;
egui::CentralPanel::default()
.frame(egui::Frame::default().fill(Color32::from_rgb(0x43, 0x20, 0x62)))
.show(&ctx, |ui| {
egui::Frame::none()
.fill(Color32::from_rgb(0x0F, 0x0F, 0x0F))
.rounding(Rounding::same(20.0))
.outer_margin(outer_margin)
.inner_margin(inner_margin)
.show(ui, |ui| {
let desired_height = 630.0 - total_margin * 2.0;
let desired_width = 1200.0 - total_margin * 2.0;
let desired_size = egui::vec2(desired_width, desired_height);
ui.set_min_height(desired_height); // Set minimum height for the container
ui.set_min_width(desired_width); // Set minimum width for the container
//
ui.centered_and_justified(|ui| {
egui::ScrollArea::vertical().show(ui, |ui| {
//ui.spacing_mut().item_spacing = egui::vec2(0.0, 0.0);
//ui.vertical(|ui| {
wrapped_body(ui, &note.note.content);
ui.horizontal(|ui| {
ui.image(&pfp);
render_username(app, ui, &note.profile);
});
//});
});
})
})
});
}
fn profile_ui(app: &Notecrumbs, ctx: &egui::Context, profile: &ProfileRenderData) {
use egui::{FontId, RichText};
let pfp = ctx.load_texture("pfp", profile.pfp.clone(), Default::default());
setup_visuals(&app.font_data, ctx);
egui::CentralPanel::default().show(&ctx, |ui| {
ui.horizontal(|ui| {
ui.label(RichText::new("").font(FontId::proportional(120.0)));
ui.vertical(|ui| {
ui.label(RichText::new(content).font(FontId::proportional(40.0)));
ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.image(&pfp);
render_username(app, ui, &profile);
});
})
//body(ui, &profile.about);
});
});
}
pub fn render_note(app: &Notecrumbs, content: &str) -> Vec<u8> {
pub fn render_note(app: &Notecrumbs, render_data: &RenderData) -> Vec<u8> {
use egui_skia::{rasterize, RasterizeOptions};
use skia_safe::EncodedImageFormat;
@ -33,7 +426,19 @@ pub fn render_note(app: &Notecrumbs, content: &str) -> Vec<u8> {
frames_before_screenshot: 1,
};
let mut surface = rasterize((1200, 630), |ctx| note_ui(app, ctx, content), Some(options));
let mut surface = match render_data {
RenderData::Note(note_render_data) => rasterize(
(1200, 630),
|ctx| note_ui(app, ctx, note_render_data),
Some(options),
),
RenderData::Profile(profile_render_data) => rasterize(
(1200, 630),
|ctx| profile_ui(app, ctx, profile_render_data),
Some(options),
),
};
surface
.image_snapshot()