selectable text option
Add a selectable text option to various note views. We don't want selection events to interfere with back drag, so this is the first step toward ensure back drag works. Vertical scrollviews also interfere with back drag, so we'll still need a way to compose gestures. It's not clear if this is currently possibly with egui (union of responses somehow maybe?) Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
@@ -138,6 +138,8 @@ impl NewThreadNotes {
|
|||||||
/// Simple helper for processing a NewThreadNotes result. It simply
|
/// Simple helper for processing a NewThreadNotes result. It simply
|
||||||
/// inserts/merges the notes into the thread cache
|
/// inserts/merges the notes into the thread cache
|
||||||
pub fn process(&self, thread: &mut Thread) {
|
pub fn process(&self, thread: &mut Thread) {
|
||||||
thread.view.insert(&self.notes);
|
// threads are chronological, ie reversed from reverse-chronological, the default.
|
||||||
|
let reversed = true;
|
||||||
|
thread.view.insert(&self.notes, reversed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,12 +109,20 @@ impl<'a> TimelineSource<'a> {
|
|||||||
new_refs.push((note, NoteRef { key, created_at }));
|
new_refs.push((note, NoteRef { key, created_at }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We're assuming reverse-chronological here (timelines). This
|
||||||
|
// flag ensures we trigger the items_inserted_at_start
|
||||||
|
// optimization in VirtualList. We need this flag because we can
|
||||||
|
// insert notes into chronological order sometimes, and this
|
||||||
|
// optimization doesn't make sense in those situations.
|
||||||
|
let reversed = false;
|
||||||
|
|
||||||
// ViewFilter::NotesAndReplies
|
// ViewFilter::NotesAndReplies
|
||||||
{
|
{
|
||||||
let refs: Vec<NoteRef> = new_refs.iter().map(|(_note, nr)| *nr).collect();
|
let refs: Vec<NoteRef> = new_refs.iter().map(|(_note, nr)| *nr).collect();
|
||||||
|
|
||||||
|
let reversed = false;
|
||||||
self.view(app, txn, ViewFilter::NotesAndReplies)
|
self.view(app, txn, ViewFilter::NotesAndReplies)
|
||||||
.insert(&refs);
|
.insert(&refs, reversed);
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -133,7 +141,7 @@ impl<'a> TimelineSource<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.view(app, txn, ViewFilter::Notes)
|
self.view(app, txn, ViewFilter::Notes)
|
||||||
.insert(&filtered_refs);
|
.insert(&filtered_refs, reversed);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -211,7 +219,7 @@ impl TimelineTab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert(&mut self, new_refs: &[NoteRef]) {
|
pub fn insert(&mut self, new_refs: &[NoteRef], reversed: bool) {
|
||||||
if new_refs.is_empty() {
|
if new_refs.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -228,7 +236,14 @@ impl TimelineTab {
|
|||||||
match merge_kind {
|
match merge_kind {
|
||||||
// TODO: update egui_virtual_list to support spliced inserts
|
// TODO: update egui_virtual_list to support spliced inserts
|
||||||
MergeKind::Spliced => list.reset(),
|
MergeKind::Spliced => list.reset(),
|
||||||
MergeKind::FrontInsert => list.items_inserted_at_start(new_items),
|
MergeKind::FrontInsert => {
|
||||||
|
// only run this logic if we're reverse-chronological
|
||||||
|
// reversed in this case means chronological, since the
|
||||||
|
// default is reverse-chronological. yeah it's confusing.
|
||||||
|
if !reversed {
|
||||||
|
list.items_inserted_at_start(new_items);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,26 @@ pub struct Mention<'a> {
|
|||||||
app: &'a mut Damus,
|
app: &'a mut Damus,
|
||||||
txn: &'a Transaction,
|
txn: &'a Transaction,
|
||||||
pk: &'a [u8; 32],
|
pk: &'a [u8; 32],
|
||||||
|
selectable: bool,
|
||||||
size: f32,
|
size: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Mention<'a> {
|
impl<'a> Mention<'a> {
|
||||||
pub fn new(app: &'a mut Damus, txn: &'a Transaction, pk: &'a [u8; 32]) -> Self {
|
pub fn new(app: &'a mut Damus, txn: &'a Transaction, pk: &'a [u8; 32]) -> Self {
|
||||||
let size = 16.0;
|
let size = 16.0;
|
||||||
Mention { app, txn, pk, size }
|
let selectable = true;
|
||||||
|
Mention {
|
||||||
|
app,
|
||||||
|
txn,
|
||||||
|
pk,
|
||||||
|
selectable,
|
||||||
|
size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selectable(mut self, selectable: bool) -> Self {
|
||||||
|
self.selectable = selectable;
|
||||||
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn size(mut self, size: f32) -> Self {
|
pub fn size(mut self, size: f32) -> Self {
|
||||||
@@ -22,7 +35,7 @@ impl<'a> Mention<'a> {
|
|||||||
|
|
||||||
impl<'a> egui::Widget for Mention<'a> {
|
impl<'a> egui::Widget for Mention<'a> {
|
||||||
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
|
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
|
||||||
mention_ui(self.app, self.txn, self.pk, ui, self.size)
|
mention_ui(self.app, self.txn, self.pk, ui, self.size, self.selectable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +45,7 @@ fn mention_ui(
|
|||||||
pk: &[u8; 32],
|
pk: &[u8; 32],
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
size: f32,
|
size: f32,
|
||||||
|
selectable: bool
|
||||||
) -> egui::Response {
|
) -> egui::Response {
|
||||||
#[cfg(feature = "profiling")]
|
#[cfg(feature = "profiling")]
|
||||||
puffin::profile_function!();
|
puffin::profile_function!();
|
||||||
@@ -46,9 +60,10 @@ fn mention_ui(
|
|||||||
"??".to_string()
|
"??".to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
let resp = ui.add(egui::Label::new(
|
let resp = ui.add(
|
||||||
egui::RichText::new(name).color(colors::PURPLE).size(size),
|
egui::Label::new(egui::RichText::new(name).color(colors::PURPLE).size(size))
|
||||||
));
|
.selectable(selectable),
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(rec) = profile.as_ref() {
|
if let Some(rec) = profile.as_ref() {
|
||||||
resp.on_hover_ui_at_pointer(|ui| {
|
resp.on_hover_ui_at_pointer(|ui| {
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ fn render_note_contents(
|
|||||||
#[cfg(feature = "profiling")]
|
#[cfg(feature = "profiling")]
|
||||||
puffin::profile_function!();
|
puffin::profile_function!();
|
||||||
|
|
||||||
|
let selectable = options.has_selectable_text();
|
||||||
let images: Vec<String> = vec![];
|
let images: Vec<String> = vec![];
|
||||||
let mut inline_note: Option<(&[u8; 32], &str)> = None;
|
let mut inline_note: Option<(&[u8; 32], &str)> = None;
|
||||||
|
|
||||||
@@ -173,7 +174,7 @@ fn render_note_contents(
|
|||||||
BlockType::Text => {
|
BlockType::Text => {
|
||||||
#[cfg(feature = "profiling")]
|
#[cfg(feature = "profiling")]
|
||||||
puffin::profile_scope!("text contents");
|
puffin::profile_scope!("text contents");
|
||||||
ui.label(block.as_str());
|
ui.add(egui::Label::new(block.as_str()).selectable(selectable));
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
|
|||||||
@@ -33,11 +33,17 @@ fn reply_desc(ui: &mut egui::Ui, txn: &Transaction, note_reply: &NoteReply, app:
|
|||||||
#[cfg(feature = "profiling")]
|
#[cfg(feature = "profiling")]
|
||||||
puffin::profile_function!();
|
puffin::profile_function!();
|
||||||
|
|
||||||
ui.add(Label::new(
|
let size = 10.0;
|
||||||
RichText::new("replying to")
|
let selectable = false;
|
||||||
.size(10.0)
|
|
||||||
.color(colors::GRAY_SECONDARY),
|
ui.add(
|
||||||
));
|
Label::new(
|
||||||
|
RichText::new("replying to")
|
||||||
|
.size(size)
|
||||||
|
.color(colors::GRAY_SECONDARY),
|
||||||
|
)
|
||||||
|
.selectable(selectable),
|
||||||
|
);
|
||||||
|
|
||||||
let reply = if let Some(reply) = note_reply.reply() {
|
let reply = if let Some(reply) = note_reply.reply() {
|
||||||
reply
|
reply
|
||||||
@@ -48,55 +54,91 @@ fn reply_desc(ui: &mut egui::Ui, txn: &Transaction, note_reply: &NoteReply, app:
|
|||||||
let reply_note = if let Ok(reply_note) = app.ndb.get_note_by_id(txn, reply.id) {
|
let reply_note = if let Ok(reply_note) = app.ndb.get_note_by_id(txn, reply.id) {
|
||||||
reply_note
|
reply_note
|
||||||
} else {
|
} else {
|
||||||
ui.add(Label::new(
|
ui.add(
|
||||||
RichText::new("a note")
|
Label::new(
|
||||||
.size(10.0)
|
RichText::new("a note")
|
||||||
.color(colors::GRAY_SECONDARY),
|
.size(size)
|
||||||
));
|
.color(colors::GRAY_SECONDARY),
|
||||||
|
)
|
||||||
|
.selectable(selectable),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if note_reply.is_reply_to_root() {
|
if note_reply.is_reply_to_root() {
|
||||||
// We're replying to the root, let's show this
|
// We're replying to the root, let's show this
|
||||||
ui.add(ui::Mention::new(app, txn, reply_note.pubkey()).size(10.0));
|
ui.add(
|
||||||
ui.add(Label::new(
|
ui::Mention::new(app, txn, reply_note.pubkey())
|
||||||
RichText::new("'s note")
|
.size(size)
|
||||||
.size(10.0)
|
.selectable(selectable),
|
||||||
.color(colors::GRAY_SECONDARY),
|
);
|
||||||
));
|
ui.add(
|
||||||
|
Label::new(
|
||||||
|
RichText::new("'s note")
|
||||||
|
.size(size)
|
||||||
|
.color(colors::GRAY_SECONDARY),
|
||||||
|
)
|
||||||
|
.selectable(selectable),
|
||||||
|
);
|
||||||
} else if let Some(root) = note_reply.root() {
|
} else if let Some(root) = note_reply.root() {
|
||||||
// replying to another post in a thread, not the root
|
// replying to another post in a thread, not the root
|
||||||
|
|
||||||
if let Ok(root_note) = app.ndb.get_note_by_id(txn, root.id) {
|
if let Ok(root_note) = app.ndb.get_note_by_id(txn, root.id) {
|
||||||
if root_note.pubkey() == reply_note.pubkey() {
|
if root_note.pubkey() == reply_note.pubkey() {
|
||||||
// simply "replying to bob's note" when replying to bob in his thread
|
// simply "replying to bob's note" when replying to bob in his thread
|
||||||
ui.add(ui::Mention::new(app, txn, reply_note.pubkey()).size(10.0));
|
ui.add(
|
||||||
ui.add(Label::new(
|
ui::Mention::new(app, txn, reply_note.pubkey())
|
||||||
RichText::new("'s note")
|
.size(size)
|
||||||
.size(10.0)
|
.selectable(selectable),
|
||||||
.color(colors::GRAY_SECONDARY),
|
);
|
||||||
));
|
ui.add(
|
||||||
|
Label::new(
|
||||||
|
RichText::new("'s note")
|
||||||
|
.size(size)
|
||||||
|
.color(colors::GRAY_SECONDARY),
|
||||||
|
)
|
||||||
|
.selectable(selectable),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// replying to bob in alice's thread
|
// replying to bob in alice's thread
|
||||||
|
|
||||||
ui.add(ui::Mention::new(app, txn, reply_note.pubkey()).size(10.0));
|
ui.add(
|
||||||
ui.add(Label::new(
|
ui::Mention::new(app, txn, reply_note.pubkey())
|
||||||
RichText::new("in").size(10.0).color(colors::GRAY_SECONDARY),
|
.size(size)
|
||||||
));
|
.selectable(selectable),
|
||||||
ui.add(ui::Mention::new(app, txn, root_note.pubkey()).size(10.0));
|
);
|
||||||
ui.add(Label::new(
|
ui.add(
|
||||||
RichText::new("'s thread")
|
Label::new(RichText::new("in").size(size).color(colors::GRAY_SECONDARY))
|
||||||
.size(10.0)
|
.selectable(selectable),
|
||||||
.color(colors::GRAY_SECONDARY),
|
);
|
||||||
));
|
ui.add(
|
||||||
|
ui::Mention::new(app, txn, root_note.pubkey())
|
||||||
|
.size(size)
|
||||||
|
.selectable(selectable),
|
||||||
|
);
|
||||||
|
ui.add(
|
||||||
|
Label::new(
|
||||||
|
RichText::new("'s thread")
|
||||||
|
.size(size)
|
||||||
|
.color(colors::GRAY_SECONDARY),
|
||||||
|
)
|
||||||
|
.selectable(selectable),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ui.add(ui::Mention::new(app, txn, reply_note.pubkey()).size(10.0));
|
ui.add(
|
||||||
ui.add(Label::new(
|
ui::Mention::new(app, txn, reply_note.pubkey())
|
||||||
RichText::new("in someone's thread")
|
.size(size)
|
||||||
.size(10.0)
|
.selectable(selectable),
|
||||||
.color(colors::GRAY_SECONDARY),
|
);
|
||||||
));
|
ui.add(
|
||||||
|
Label::new(
|
||||||
|
RichText::new("in someone's thread")
|
||||||
|
.size(size)
|
||||||
|
.color(colors::GRAY_SECONDARY),
|
||||||
|
)
|
||||||
|
.selectable(selectable),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,6 +169,11 @@ impl<'a> NoteView<'a> {
|
|||||||
self
|
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 {
|
pub fn wide(mut self, enable: bool) -> Self {
|
||||||
self.options_mut().set_wide(enable);
|
self.options_mut().set_wide(enable);
|
||||||
self
|
self
|
||||||
|
|||||||
@@ -6,20 +6,45 @@ bitflags! {
|
|||||||
#[repr(transparent)]
|
#[repr(transparent)]
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct NoteOptions: u32 {
|
pub struct NoteOptions: u32 {
|
||||||
const actionbar = 0b00000001;
|
const actionbar = 0b00000001;
|
||||||
const note_previews = 0b00000010;
|
const note_previews = 0b00000010;
|
||||||
const small_pfp = 0b00000100;
|
const small_pfp = 0b00000100;
|
||||||
const medium_pfp = 0b00001000;
|
const medium_pfp = 0b00001000;
|
||||||
const wide = 0b00010000;
|
const wide = 0b00010000;
|
||||||
|
const selectable_text = 0b00100000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! create_setter {
|
||||||
|
($fn_name:ident, $option:ident) => {
|
||||||
|
#[inline]
|
||||||
|
pub fn $fn_name(&mut self, enable: bool) {
|
||||||
|
if enable {
|
||||||
|
*self |= NoteOptions::$option;
|
||||||
|
} else {
|
||||||
|
*self &= !NoteOptions::$option;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
impl NoteOptions {
|
impl NoteOptions {
|
||||||
|
create_setter!(set_small_pfp, small_pfp);
|
||||||
|
create_setter!(set_medium_pfp, medium_pfp);
|
||||||
|
create_setter!(set_note_previews, note_previews);
|
||||||
|
create_setter!(set_selectable_text, selectable_text);
|
||||||
|
create_setter!(set_actionbar, actionbar);
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn has_actionbar(self) -> bool {
|
pub fn has_actionbar(self) -> bool {
|
||||||
(self & NoteOptions::actionbar) == NoteOptions::actionbar
|
(self & NoteOptions::actionbar) == NoteOptions::actionbar
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn has_selectable_text(self) -> bool {
|
||||||
|
(self & NoteOptions::selectable_text) == NoteOptions::selectable_text
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn has_note_previews(self) -> bool {
|
pub fn has_note_previews(self) -> bool {
|
||||||
(self & NoteOptions::note_previews) == NoteOptions::note_previews
|
(self & NoteOptions::note_previews) == NoteOptions::note_previews
|
||||||
@@ -58,40 +83,4 @@ impl NoteOptions {
|
|||||||
*self &= !NoteOptions::wide;
|
*self &= !NoteOptions::wide;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn set_small_pfp(&mut self, enable: bool) {
|
|
||||||
if enable {
|
|
||||||
*self |= NoteOptions::small_pfp;
|
|
||||||
} else {
|
|
||||||
*self &= !NoteOptions::small_pfp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn set_medium_pfp(&mut self, enable: bool) {
|
|
||||||
if enable {
|
|
||||||
*self |= NoteOptions::medium_pfp;
|
|
||||||
} else {
|
|
||||||
*self &= !NoteOptions::medium_pfp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn set_note_previews(&mut self, enable: bool) {
|
|
||||||
if enable {
|
|
||||||
*self |= NoteOptions::note_previews;
|
|
||||||
} else {
|
|
||||||
*self &= !NoteOptions::note_previews;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn set_actionbar(&mut self, enable: bool) {
|
|
||||||
if enable {
|
|
||||||
*self |= NoteOptions::actionbar;
|
|
||||||
} else {
|
|
||||||
*self &= !NoteOptions::actionbar;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ fn timeline_ui(ui: &mut egui::Ui, app: &mut Damus, timeline: usize, reversed: bo
|
|||||||
let textmode = app.textmode;
|
let textmode = app.textmode;
|
||||||
let resp = ui::NoteView::new(app, ¬e)
|
let resp = ui::NoteView::new(app, ¬e)
|
||||||
.note_previews(!textmode)
|
.note_previews(!textmode)
|
||||||
|
.selectable_text(false)
|
||||||
.show(ui);
|
.show(ui);
|
||||||
|
|
||||||
if let Some(action) = resp.action {
|
if let Some(action) = resp.action {
|
||||||
|
|||||||
Reference in New Issue
Block a user