notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit bc8ed2c6428277f38608e8220dc617ebef62e3a2
parent a3e975d133de5204307c3879636adcbb092d0b1b
Author: kernelkind <kernelkind@gmail.com>
Date:   Mon,  3 Feb 2025 17:30:01 -0500

color mentions in PostView

Signed-off-by: kernelkind <kernelkind@gmail.com>

Diffstat:
Mcrates/notedeck_columns/src/post.rs | 79++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mcrates/notedeck_columns/src/ui/note/post.rs | 31+++++++++++++++++++++++++++----
2 files changed, 105 insertions(+), 5 deletions(-)

diff --git a/crates/notedeck_columns/src/post.rs b/crates/notedeck_columns/src/post.rs @@ -1,4 +1,4 @@ -use egui::TextBuffer; +use egui::{text::LayoutJob, TextBuffer, TextFormat}; use enostr::{FullKeypair, Pubkey}; use nostrdb::{Note, NoteBuilder, NoteReply}; use std::{ @@ -351,6 +351,70 @@ impl PostBuffer { mentions, } } + + pub fn to_layout_job(&self, ui: &egui::Ui) -> LayoutJob { + let mut job = LayoutJob::default(); + let colored_fmt = default_text_format_colored(ui, crate::colors::PINK); + + let mut prev_text_char_index = 0; + let mut prev_text_byte_index = 0; + for (start_char_index, mention_ind) in &self.mention_starts { + if let Some(info) = self.mentions.get(mention_ind) { + if matches!(info.mention_type, MentionType::Finalized(_)) { + let end_char_index = info.end_index; + + let char_indices = prev_text_char_index..*start_char_index; + if let Some(byte_indicies) = + char_indices_to_byte(&self.text_buffer, char_indices.clone()) + { + if let Some(prev_text) = self.text_buffer.get(byte_indicies.clone()) { + job.append(prev_text, 0.0, default_text_format(ui)); + prev_text_char_index = *start_char_index; + prev_text_byte_index = byte_indicies.end; + } + } + + let char_indices = *start_char_index..end_char_index; + if let Some(byte_indicies) = + char_indices_to_byte(&self.text_buffer, char_indices.clone()) + { + if let Some(cur_text) = self.text_buffer.get(byte_indicies.clone()) { + job.append(cur_text, 0.0, colored_fmt.clone()); + prev_text_char_index = end_char_index; + prev_text_byte_index = byte_indicies.end; + } + } + } + } + } + + if prev_text_byte_index < self.text_buffer.len() { + if let Some(cur_text) = self.text_buffer.get(prev_text_byte_index..) { + job.append(cur_text, 0.0, default_text_format(ui)); + } else { + error!( + "could not retrieve substring from [{} to {}) in PostBuffer::text_buffer", + prev_text_byte_index, + self.text_buffer.len() + ); + } + } + + job + } +} + +fn char_indices_to_byte(text: &str, char_range: Range<usize>) -> Option<Range<usize>> { + let mut char_indices = text.char_indices(); + + let start = char_indices.nth(char_range.start)?.0; + let end = if char_range.end < text.chars().count() { + char_indices.nth(char_range.end - char_range.start - 1)?.0 + } else { + text.len() + }; + + Some(start..end) } pub fn downcast_post_buffer(buffer: &dyn TextBuffer) -> Option<&PostBuffer> { @@ -365,6 +429,19 @@ pub fn downcast_post_buffer(buffer: &dyn TextBuffer) -> Option<&PostBuffer> { } } +fn default_text_format(ui: &egui::Ui) -> TextFormat { + default_text_format_colored( + ui, + ui.visuals() + .override_text_color + .unwrap_or_else(|| ui.visuals().widgets.inactive.text_color()), + ) +} + +fn default_text_format_colored(ui: &egui::Ui, color: egui::Color32) -> TextFormat { + TextFormat::simple(egui::FontSelection::default().resolve(ui.style()), color) +} + pub struct PostOutput { pub text: String, pub mentions: Vec<Pubkey>, diff --git a/crates/notedeck_columns/src/ui/note/post.rs b/crates/notedeck_columns/src/ui/note/post.rs @@ -1,15 +1,15 @@ use crate::draft::{Draft, Drafts, MentionHint}; use crate::images::fetch_img; use crate::media_upload::{nostrbuild_nip96_upload, MediaPath}; -use crate::post::{MentionType, NewPost}; +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; +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}; +use egui::{vec2, Frame, Layout, Margin, Pos2, ScrollArea, Sense, TextBuffer}; use enostr::{FilledKeypair, FullKeypair, NoteId, Pubkey, RelayPool}; use nostrdb::{Ndb, Transaction}; @@ -130,10 +130,22 @@ impl<'a> PostView<'a> { ); } + let mut layouter = |ui: &egui::Ui, buf: &dyn TextBuffer, wrap_width: f32| { + let mut layout_job = if let Some(post_buffer) = downcast_post_buffer(buf) { + post_buffer.to_layout_job(ui) + } else { + error!("Failed to get custom mentions layouter"); + text_edit_default_layout(ui, buf.as_str().to_owned(), wrap_width) + }; + layout_job.wrap.max_width = wrap_width; + ui.fonts(|f| f.layout_job(layout_job)) + }; + let textedit = TextEdit::multiline(&mut self.draft.buffer) .hint_text(egui::RichText::new("Write a banger note here...").weak()) .frame(false) - .desired_width(ui.available_width()); + .desired_width(ui.available_width()) + .layouter(&mut layouter); let out = textedit.show(ui); @@ -584,6 +596,17 @@ fn calculate_mention_hints_pos(out: &TextEditOutput, char_pos: usize) -> egui::P out.text_clip_rect.left_bottom() } +fn text_edit_default_layout(ui: &egui::Ui, text: String, wrap_width: f32) -> LayoutJob { + LayoutJob::simple( + text, + egui::FontSelection::default().resolve(ui.style()), + ui.visuals() + .override_text_color + .unwrap_or_else(|| ui.visuals().widgets.inactive.text_color()), + wrap_width, + ) +} + mod preview { use crate::media_upload::Nip94Event;