use std::{collections::HashSet, time::Instant};
use ratatui_image::picker::Picker;
use crate::{
discord::{AppCommand, AppEvent},
tui::ui::EmojiImage,
};
use super::{
EmojiImageTarget,
cache::{MediaImageCacheCore, MediaImageCacheEntry, RenderProtocolCache},
decode::{DecodedMediaImage, MediaImageDecodeKey, MediaImageDecodeRequest},
protocol_job::{MediaProtocolBuildJob, MediaProtocolBuildResult, MediaProtocolBuildTarget},
work::{MediaWorkError, MediaWorkResult},
};
pub(super) const MAX_EMOJI_IMAGE_CACHE_ENTRIES: usize = 128;
const EMOJI_IMAGE_CACHE_DECODED_BYTE_BUDGET: u64 = 64 * 1024 * 1024;
pub(in crate::tui) struct EmojiImageCache {
pub(super) picker: Option<Picker>,
pub(super) cache: MediaImageCacheCore<String, EmojiImageEntry>,
pub(super) protocol_jobs: Vec<MediaProtocolBuildJob>,
}
pub(super) enum EmojiImageEntry {
Loading {
last_used: u64,
},
Decoding {
generation: u64,
last_used: u64,
},
Ready {
generation: u64,
image: DecodedMediaImage,
protocols: Box<RenderProtocolCache<usize>>,
last_used: u64,
},
Failed {
last_used: u64,
},
}
impl MediaImageCacheEntry for EmojiImageEntry {
fn last_used(&self) -> u64 {
match self {
EmojiImageEntry::Loading { last_used }
| EmojiImageEntry::Decoding { last_used, .. }
| EmojiImageEntry::Ready { last_used, .. }
| EmojiImageEntry::Failed { last_used } => *last_used,
}
}
fn decoded_image(&self) -> Option<&DecodedMediaImage> {
match self {
EmojiImageEntry::Ready { image, .. } => Some(image),
EmojiImageEntry::Loading { .. }
| EmojiImageEntry::Decoding { .. }
| EmojiImageEntry::Failed { .. } => None,
}
}
fn decoded_image_mut(&mut self) -> Option<&mut DecodedMediaImage> {
match self {
EmojiImageEntry::Ready { image, .. } => Some(image),
EmojiImageEntry::Loading { .. }
| EmojiImageEntry::Decoding { .. }
| EmojiImageEntry::Failed { .. } => None,
}
}
fn touch(&mut self, tick: u64) {
match self {
EmojiImageEntry::Loading { last_used }
| EmojiImageEntry::Decoding { last_used, .. }
| EmojiImageEntry::Ready { last_used, .. }
| EmojiImageEntry::Failed { last_used } => *last_used = tick,
}
}
fn is_loading(&self) -> bool {
matches!(self, EmojiImageEntry::Loading { .. })
}
fn decoding_generation(&self) -> Option<u64> {
match self {
EmojiImageEntry::Decoding { generation, .. } => Some(*generation),
EmojiImageEntry::Loading { .. }
| EmojiImageEntry::Ready { .. }
| EmojiImageEntry::Failed { .. } => None,
}
}
}
impl EmojiImageCache {
pub(in crate::tui) fn new(picker: Option<Picker>) -> Self {
Self {
picker,
cache: MediaImageCacheCore::new(),
protocol_jobs: Vec::new(),
}
}
pub(in crate::tui) fn render_state(
&mut self,
targets: &[EmojiImageTarget],
) -> Vec<EmojiImage<'_>> {
for target in targets {
let touch_tick = self.cache.next_tick();
if let Some(entry) = self.cache.entries.get_mut(&target.url) {
entry.touch(touch_tick);
if let EmojiImageEntry::Ready {
generation,
image,
protocols,
..
} = entry
&& let Some(picker) = self.picker.as_ref()
&& protocols.request_build(&image.current_frame_index())
{
self.protocol_jobs.push(MediaProtocolBuildJob::emoji(
target.url.clone(),
*generation,
image.current_frame_index(),
picker.clone(),
image.current_frame_shared(),
));
}
}
}
targets
.iter()
.filter_map(|target| {
let EmojiImageEntry::Ready {
image, protocols, ..
} = self.cache.entries.get(&target.url)?
else {
return None;
};
Some(EmojiImage {
url: target.url.clone(),
protocol: protocols.get_or_last(&image.current_frame_index())?,
})
})
.collect()
}
pub(in crate::tui) fn next_requests(
&mut self,
targets: &[EmojiImageTarget],
) -> Vec<AppCommand> {
if self.picker.is_none() {
return Vec::new();
}
let mut intents = Vec::new();
for target in targets.iter().take(MAX_EMOJI_IMAGE_CACHE_ENTRIES) {
if self
.cache
.insert_loading(target.url.clone(), |last_used| EmojiImageEntry::Loading {
last_used,
})
{
intents.push(AppCommand::LoadAttachmentPreview {
url: target.url.clone(),
});
}
}
self.prune_to_limit(targets);
intents
}
pub(in crate::tui) fn record_event(
&mut self,
event: &AppEvent,
) -> Option<MediaImageDecodeRequest> {
match event {
AppEvent::AttachmentPreviewLoaded { url, .. } => self.store_loaded(url),
AppEvent::AttachmentPreviewLoadFailed { url, .. } => {
self.store_failed(url);
None
}
_ => None,
}
}
pub(super) fn prune_to_limit(&mut self, targets: &[EmojiImageTarget]) {
let protected: HashSet<&str> = targets
.iter()
.take(MAX_EMOJI_IMAGE_CACHE_ENTRIES)
.map(|target| target.url.as_str())
.collect();
self.cache.prune_to_limits(
MAX_EMOJI_IMAGE_CACHE_ENTRIES,
EMOJI_IMAGE_CACHE_DECODED_BYTE_BUDGET,
|url| protected.contains(url.as_str()),
);
}
fn store_loaded(&mut self, url: &str) -> Option<MediaImageDecodeRequest> {
self.cache.start_decode_request(
url.to_owned(),
self.picker.is_some(),
|generation, last_used| EmojiImageEntry::Decoding {
generation,
last_used,
},
|last_used| EmojiImageEntry::Failed { last_used },
MediaImageDecodeKey::Emoji,
)
}
pub(in crate::tui) fn store_decoded(
&mut self,
url: String,
result_generation: u64,
result: MediaWorkResult<DecodedMediaImage>,
) {
if !self
.cache
.decoded_generation_matches(&url, result_generation)
{
return;
}
let last_used = self.cache.next_tick();
match result {
Ok(image) => {
if self.picker.is_none() {
self.cache
.entries
.insert(url, EmojiImageEntry::Failed { last_used });
return;
}
self.cache.entries.insert(
url,
EmojiImageEntry::Ready {
generation: result_generation,
image,
protocols: Box::new(RenderProtocolCache::new()),
last_used,
},
);
}
Err(MediaWorkError::Busy) => {
self.cache.entries.remove(&url);
}
Err(MediaWorkError::Failed(_)) => {
self.cache
.entries
.insert(url, EmojiImageEntry::Failed { last_used });
}
}
}
fn store_failed(&mut self, url: &str) {
self.cache
.store_failed_if_present(url.to_owned(), |last_used| EmojiImageEntry::Failed {
last_used,
});
}
pub(in crate::tui) fn sync_animation_visibility(
&mut self,
targets: &[EmojiImageTarget],
now: Instant,
) {
let visible = targets
.iter()
.map(|target| target.url.as_str())
.collect::<HashSet<_>>();
for (url, entry) in &mut self.cache.entries {
let EmojiImageEntry::Ready {
image, protocols, ..
} = entry
else {
continue;
};
if visible.contains(url.as_str()) && !protocols.is_empty() {
image.start_animation(now);
} else {
image.pause_animation();
}
}
}
pub(in crate::tui) fn pause_animations(&mut self) {
self.cache.pause_animations();
}
pub(in crate::tui) fn next_animation_deadline(&self) -> Option<Instant> {
self.cache.next_animation_deadline()
}
pub(in crate::tui) fn advance_animations(&mut self, now: Instant) -> bool {
self.cache.advance_animations(now)
}
pub(in crate::tui) fn take_protocol_jobs(&mut self) -> Vec<MediaProtocolBuildJob> {
std::mem::take(&mut self.protocol_jobs)
}
pub(in crate::tui) fn store_protocol(&mut self, completed: MediaProtocolBuildResult) {
let MediaProtocolBuildTarget::Emoji { url, frame_index } = completed.target else {
return;
};
let failed = match self.cache.entries.get_mut(&url) {
Some(EmojiImageEntry::Ready {
generation,
protocols,
..
}) if *generation == completed.generation => protocols
.store_result(frame_index, completed.result)
.is_err(),
_ => false,
};
if failed {
let last_used = self.cache.next_tick();
self.cache
.entries
.insert(url, EmojiImageEntry::Failed { last_used });
}
}
}