diff --git a/client-sdk/api.golden b/client-sdk/api.golden index c49d044..dc27b83 100644 --- a/client-sdk/api.golden +++ b/client-sdk/api.golden @@ -793,9 +793,15 @@ pub enum clipboard_history_client_sdk::ui_actor::UiEntryCache pub clipboard_history_client_sdk::ui_actor::UiEntryCache::Binary pub clipboard_history_client_sdk::ui_actor::UiEntryCache::Binary::mime_type: alloc::boxed::Box pub clipboard_history_client_sdk::ui_actor::UiEntryCache::Error(clipboard_history_core::Error) +pub clipboard_history_client_sdk::ui_actor::UiEntryCache::HighlightedText +pub clipboard_history_client_sdk::ui_actor::UiEntryCache::HighlightedText::end: usize +pub clipboard_history_client_sdk::ui_actor::UiEntryCache::HighlightedText::one_liner: alloc::boxed::Box +pub clipboard_history_client_sdk::ui_actor::UiEntryCache::HighlightedText::start: usize pub clipboard_history_client_sdk::ui_actor::UiEntryCache::Image pub clipboard_history_client_sdk::ui_actor::UiEntryCache::Text pub clipboard_history_client_sdk::ui_actor::UiEntryCache::Text::one_liner: alloc::boxed::Box +impl clipboard_history_client_sdk::ui_actor::UiEntryCache +pub const fn clipboard_history_client_sdk::ui_actor::UiEntryCache::is_text(&self) -> bool impl core::fmt::Debug for clipboard_history_client_sdk::ui_actor::UiEntryCache pub fn clipboard_history_client_sdk::ui_actor::UiEntryCache::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result impl core::marker::Freeze for clipboard_history_client_sdk::ui_actor::UiEntryCache diff --git a/client-sdk/src/ui_actor.rs b/client-sdk/src/ui_actor.rs index a4c13d6..b60c720 100644 --- a/client-sdk/src/ui_actor.rs +++ b/client-sdk/src/ui_actor.rs @@ -121,12 +121,31 @@ pub struct UiEntry { #[derive(Debug)] pub enum UiEntryCache { - Text { one_liner: Box }, + Text { + one_liner: Box, + }, + HighlightedText { + one_liner: Box, + start: usize, + end: usize, + }, Image, - Binary { mime_type: Box }, + Binary { + mime_type: Box, + }, Error(CoreError), } +impl UiEntryCache { + #[must_use] + pub const fn is_text(&self) -> bool { + match self { + Self::Text { .. } | Self::HighlightedText { .. } => true, + Self::Image | Self::Binary { .. } | Self::Error(_) => false, + } + } +} + #[derive(Debug)] pub struct DetailedEntry { pub mime_type: Box, @@ -352,7 +371,7 @@ fn handle_command<'a, Server: AsFd, PasteServer: AsFd, E>( fn ui_entry( entry: Entry, reader: &mut EntryReader, - highlight: Option<(usize, usize)>, + mut highlight: Option<(usize, usize)>, ) -> Result { let loaded = entry.to_slice(reader)?; let mime_type = &*loaded.mime_type()?; @@ -363,7 +382,7 @@ fn ui_entry( }); } - let prefix_free = if let Some((start, _)) = highlight { + let prefix_free = if let Some((start, end)) = &mut highlight { let mut l = &loaded[start.saturating_sub(24)..]; for &b in l.iter().take(3) { // https://github.com/rust-lang/rust/blob/33422e72c8a66bdb5ee21246a948a1a02ca91674/library/core/src/num/mod.rs#L1090 @@ -374,6 +393,11 @@ fn ui_entry( } l = &l[1..]; } + + let diff = loaded.len() - l.len(); + *start -= diff; + *end -= diff; + l } else { &loaded @@ -399,10 +423,22 @@ fn ui_entry( if prefix_free.len() != loaded.len() { one_liner.push('…'); + if let Some((start, end)) = &mut highlight { + *start += '…'.len_utf8(); + *end += '…'.len_utf8(); + } } let mut prev_char_is_whitespace = false; for c in s.chars() { if (prev_char_is_whitespace || one_liner.is_empty()) && c.is_whitespace() { + if let Some((start, end)) = &mut highlight { + if one_liner.len() < *start { + *start -= c.len_utf8(); + } + if one_liner.len() < *end { + *end -= c.len_utf8(); + } + } continue; } @@ -412,11 +448,22 @@ fn ui_entry( if suffix_free.len() != prefix_free.len() { one_liner.push('…'); } + if let Some((_, end)) = &mut highlight { + *end = min(*end, one_liner.len()); + } UiEntry { entry, - cache: UiEntryCache::Text { - one_liner: one_liner.into(), + cache: if let Some((start, end)) = highlight { + UiEntryCache::HighlightedText { + one_liner: one_liner.into(), + start, + end, + } + } else { + UiEntryCache::Text { + one_liner: one_liner.into(), + } }, } }, @@ -563,12 +610,19 @@ fn do_search( unsafe { database.get(id) }? }; - Ok( - ui_entry(entry, reader, Some((start, end))).unwrap_or_else(|e| UiEntry { - cache: UiEntryCache::Error(e), - entry, - }), + Ok(ui_entry( + entry, + reader, + if start == end { + None + } else { + Some((start, end)) + }, ) + .unwrap_or_else(|e| UiEntry { + cache: UiEntryCache::Error(e), + entry, + })) }) .collect(); *search_result_buf = results; diff --git a/egui/src/main.rs b/egui/src/main.rs index 713a29e..86052c5 100644 --- a/egui/src/main.rs +++ b/egui/src/main.rs @@ -16,8 +16,9 @@ use std::{ use eframe::{ egui, egui::{ - text::LayoutJob, Align, CentralPanel, Event, FontId, FontTweak, Image, InputState, Key, - Label, Layout, Modifiers, PopupCloseBehavior, Pos2, Response, RichText, ScrollArea, Sense, + text::{LayoutJob, LayoutSection}, + Align, CentralPanel, Event, FontId, FontTweak, Image, InputState, Key, Label, Layout, + Modifiers, PopupCloseBehavior, Pos2, Response, RichText, ScrollArea, Sense, Stroke, TextEdit, TextFormat, TopBottomPanel, Ui, Vec2, ViewportBuilder, ViewportCommand, Widget, }, epaint::FontFamily, @@ -584,24 +585,11 @@ fn entry_ui( max_popup_height: f32, index: usize, ) { - let response = match &entry.cache { - UiEntryCache::Text { one_liner } => { - let mut job = LayoutJob::single_section( - one_liner.to_string(), - TextFormat { - font_id: FontId::new(16., entry_text_font.clone()), - color: ui.visuals().text_color(), - ..Default::default() - }, - ); - job.wrap = egui::text::TextWrapping { - max_rows: 1, - break_anywhere: true, - ..Default::default() - }; + macro_rules! response { + ($w:expr) => { row_ui( ui, - Label::new(job).selectable(false), + $w, state, requests, refresh, @@ -611,34 +599,71 @@ fn entry_ui( max_popup_height, index, ) + }; + } + let response = match &entry.cache { + UiEntryCache::Text { one_liner } | UiEntryCache::HighlightedText { one_liner, .. } => { + let job = LayoutJob { + text: one_liner.to_string(), + break_on_newline: false, + wrap: egui::text::TextWrapping { + max_rows: 1, + break_anywhere: true, + ..Default::default() + }, + sections: { + let format = TextFormat { + font_id: FontId::new(16., entry_text_font.clone()), + color: ui.visuals().text_color(), + ..Default::default() + }; + if let UiEntryCache::HighlightedText { + one_liner: _, + start, + end, + } = entry.cache + { + vec![ + LayoutSection { + leading_space: 0.0, + byte_range: 0..start, + format: format.clone(), + }, + LayoutSection { + leading_space: 0.0, + byte_range: start..end, + format: TextFormat { + underline: Stroke::new(1., ui.visuals().strong_text_color()), + ..format.clone() + }, + }, + LayoutSection { + leading_space: 0.0, + byte_range: end..one_liner.len(), + format, + }, + ] + } else { + vec![LayoutSection { + leading_space: 0.0, + byte_range: 0..one_liner.len(), + format, + }] + } + }, + ..LayoutJob::default() + }; + response!(Label::new(job).selectable(false)) } - UiEntryCache::Image => row_ui( - ui, + UiEntryCache::Image => response!( Image::new(format!("ringboard://{}", entry.entry.id())) .max_height(250.) .max_width(ui.available_width()) - .fit_to_original_size(1.), - state, - requests, - refresh, - entry, - try_scroll, - try_popup, - max_popup_height, - index, + .fit_to_original_size(1.) ), - UiEntryCache::Binary { mime_type } => row_ui( - ui, + UiEntryCache::Binary { mime_type } => response!( Label::new(format!("Unable to display format of type {mime_type:?}.")) - .selectable(false), - state, - requests, - refresh, - entry, - try_scroll, - try_popup, - max_popup_height, - index, + .selectable(false) ), UiEntryCache::Error(e) => { show_error(ui, e); @@ -717,7 +742,7 @@ fn row_ui( state.detailed_entry = None; let _ = requests.send(Command::GetDetails { id: entry_id, - with_text: matches!(cache, UiEntryCache::Text { .. }), + with_text: cache.is_text(), }); } diff --git a/tui/src/main.rs b/tui/src/main.rs index 536f836..731154a 100644 --- a/tui/src/main.rs +++ b/tui/src/main.rs @@ -25,7 +25,7 @@ use ratatui::{ }, layout::{Alignment, Constraint, Layout, Rect}, style::{Modifier, Style, Stylize}, - text::Line, + text::{Line, Span}, widgets::{ Block, Borders, HighlightSpacing, List, ListState, Padding, Paragraph, StatefulWidget, Widget, Wrap, @@ -374,7 +374,7 @@ fn maybe_get_details(entries: &UiEntries, ui: &mut UiState, requests: &Sender { fn ui_entry_line(UiEntry { entry: _, cache }: &UiEntry) -> Line { match cache { + &UiEntryCache::HighlightedText { + ref one_liner, + start, + end, + } => Line::default().spans([ + Span::raw(&one_liner[..start]), + Span::styled(&one_liner[start..end], Modifier::UNDERLINED), + Span::raw(&one_liner[end..]), + ]), UiEntryCache::Text { one_liner } => Line::raw(&**one_liner), UiEntryCache::Image => Line::raw("Image: open details to view.").italic(), UiEntryCache::Binary { mime_type } => {