diff --git a/rustfmt.toml b/rustfmt.toml index c1578aa..e69de29 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1 +0,0 @@ -imports_granularity = "Module" diff --git a/src/app.rs b/src/app.rs index 92a0928..6bea886 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,5 +1,8 @@ use crate::app::iced::event::listen_raw; use crate::subscriptions::launcher; +use crate::thumbnails::loader::load_thumbnail; +use crate::thumbnails::model::ThumbnailResult; +use crate::thumbnails::{provider::ThumbnailProvider, view::thumbnail_view}; use crate::{components, fl}; use clap::Parser; use cosmic::app::{Core, CosmicFlags, Settings, Task}; @@ -161,6 +164,7 @@ pub struct CosmicLauncher { needs_clear: bool, hand_over: String, dummy_id: Option, + thumbnail_provider: ThumbnailProvider, } #[derive(Debug, Clone)] @@ -186,6 +190,7 @@ pub enum Message { AltRelease, Overlap(OverlapNotifyEvent), Surface(surface::Action), + ThumbnailLoaded(ThumbnailResult), } impl CosmicLauncher { @@ -365,7 +370,9 @@ impl cosmic::Application for CosmicLauncher { needs_clear: false, hand_over: String::default(), dummy_id: None, + thumbnail_provider: ThumbnailProvider::new(), }; + let task = app.create_dummy_layer_surface(); app.needs_clear = false; @@ -540,6 +547,9 @@ impl cosmic::Application for CosmicLauncher { let b = i32::from(b.window.is_none()); a.cmp(&b) }); + if self.alt_tab { + self.thumbnail_provider.retain_visible_windows(list.iter()); + } self.launcher_items.splice(.., list); if self.result_ids.len() < self.launcher_items.len() { self.result_ids.extend( @@ -595,6 +605,19 @@ impl cosmic::Application for CosmicLauncher { .collect(); let mut cmds = Vec::new(); + if self.alt_tab { + let thumbnail_requests = self + .launcher_items + .iter() + .filter_map(|item| self.thumbnail_provider.request_thumbnail(item)) + .collect::>(); + + for request in thumbnail_requests { + cmds.push(Task::perform(load_thumbnail(request), |result| { + cosmic::action::app(Message::ThumbnailLoaded(result)) + })); + } + } while let Some(element) = self.queue.pop_front() { let updated = self.update(element); @@ -753,6 +776,10 @@ impl cosmic::Application for CosmicLauncher { cosmic::app::Action::Surface(a), )); } + Message::ThumbnailLoaded(result) => { + self.thumbnail_provider + .set_thumbnail_state(result.window, result.state); + } } Task::none() } @@ -937,6 +964,14 @@ impl cosmic::Application for CosmicLauncher { ); } + if self.alt_tab { + if let Some(thumbnail) = + thumbnail_view(self.thumbnail_provider.thumbnail_state(item)) + { + button_content.push(thumbnail); + } + } + button_content.push(column![name, desc].width(Length::FillPortion(5)).into()); if i < 10 { button_content.push( diff --git a/src/main.rs b/src/main.rs index 99b502b..bc1941b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ mod config; mod app; mod localize; mod subscriptions; +mod thumbnails; use tracing::info; use localize::localize; diff --git a/src/thumbnails/loader.rs b/src/thumbnails/loader.rs new file mode 100644 index 0000000..fb11dd7 --- /dev/null +++ b/src/thumbnails/loader.rs @@ -0,0 +1,32 @@ +use cosmic::iced::widget::image; + +use crate::thumbnails::{ + ThumbnailRequest, ThumbnailState, + model::{ThumbnailImage, ThumbnailImageSource, ThumbnailResult}, +}; + +async fn load_source(request: &ThumbnailRequest) -> Option { + request.source.as_ref().map(|source| { + ThumbnailImageSource::ImageHandle(image::Handle::from_rgba( + source.width, + source.height, + source.pixels.clone(), + )) + }) +} + +pub async fn load_thumbnail(request: ThumbnailRequest) -> ThumbnailResult { + let state = match load_source(&request).await { + Some(source) => ThumbnailState::Ready(ThumbnailImage { + window: request.window, + source, + }), + + None => ThumbnailState::Unavailable, + }; + + ThumbnailResult { + window: request.window, + state: state, + } +} diff --git a/src/thumbnails/mod.rs b/src/thumbnails/mod.rs new file mode 100644 index 0000000..07909f7 --- /dev/null +++ b/src/thumbnails/mod.rs @@ -0,0 +1,9 @@ +pub mod loader; +pub mod model; +pub mod provider; +pub mod view; + +pub use model::{ThumbnailRequest, ThumbnailState}; + +pub const THUMBNAIL_WIDTH: u32 = 160; +pub const THUMBNAIL_HEIGHT: u32 = 90; diff --git a/src/thumbnails/model.rs b/src/thumbnails/model.rs new file mode 100644 index 0000000..1dae13a --- /dev/null +++ b/src/thumbnails/model.rs @@ -0,0 +1,45 @@ +use cosmic::iced::widget::image; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct WindowKey { + pub group: u32, + pub id: u32, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ThumbnailState { + Placeholder, + Loading, + Ready(ThumbnailImage), + Unavailable, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ThumbnailImageSource { + ImageHandle(image::Handle), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ThumbnailImage { + pub window: WindowKey, + pub source: ThumbnailImageSource, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ThumbnailRequest { + pub window: WindowKey, + pub source: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ThumbnailResult { + pub window: WindowKey, + pub state: ThumbnailState, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RgbaThumbnailData { + pub width: u32, + pub height: u32, + pub pixels: Vec, +} diff --git a/src/thumbnails/provider.rs b/src/thumbnails/provider.rs new file mode 100644 index 0000000..697b1f3 --- /dev/null +++ b/src/thumbnails/provider.rs @@ -0,0 +1,123 @@ +use std::{ + collections::{HashMap, HashSet}, + hash::{DefaultHasher, Hash, Hasher}, +}; + +use pop_launcher::SearchResult; + +use crate::thumbnails::{ + ThumbnailRequest, ThumbnailState, + model::{RgbaThumbnailData, WindowKey}, +}; + +#[derive(Debug, Clone)] +struct CachedThumbnail { + signature: Option, + state: ThumbnailState, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct ThumbnailSignature { + width: u32, + height: u32, + len: usize, + hash: u64, +} + +#[derive(Debug, Default, Clone)] +pub struct ThumbnailProvider { + cache: HashMap, +} + +impl ThumbnailProvider { + pub fn new() -> Self { + Self { + cache: HashMap::new(), + } + } + + fn thumbnail_signature(item: &SearchResult) -> Option { + let thumbnail = item.thumbnail.as_ref()?; + + let mut hasher = DefaultHasher::new(); + thumbnail.pixels.hash(&mut hasher); + + Some(ThumbnailSignature { + width: thumbnail.width, + height: thumbnail.height, + len: thumbnail.pixels.len(), + hash: hasher.finish(), + }) + } + + pub fn thumbnail_state(&self, item: &SearchResult) -> ThumbnailState { + let Some(window_key) = Self::window_key(item) else { + return ThumbnailState::Unavailable; + }; + + self.cache + .get(&window_key) + .map(|cached| cached.state.clone()) + .unwrap_or(ThumbnailState::Placeholder) + } + + pub fn request_thumbnail(&mut self, item: &SearchResult) -> Option { + let window_key = Self::window_key(item)?; + let signature = Self::thumbnail_signature(item); + + let should_request = match self.cache.get(&window_key) { + Some(CachedThumbnail { + signature: cached_signature, + state: ThumbnailState::Ready(_) | ThumbnailState::Loading, + }) if *cached_signature == signature => false, + + _ => true, + }; + + if !should_request { + return None; + } + + self.cache.insert( + window_key, + CachedThumbnail { + signature, + state: ThumbnailState::Loading, + }, + ); + + Some(ThumbnailRequest { + window: window_key, + source: item.thumbnail.as_ref().map(|thumbnail| RgbaThumbnailData { + width: thumbnail.width, + height: thumbnail.height, + pixels: thumbnail.pixels.clone(), + }), + }) + } + + pub fn set_thumbnail_state(&mut self, window_key: WindowKey, state: ThumbnailState) { + if let Some(cached) = self.cache.get_mut(&window_key) { + cached.state = state; + } else { + self.cache.insert( + window_key, + CachedThumbnail { + signature: None, + state, + }, + ); + } + } + + pub fn window_key(item: &SearchResult) -> Option { + item.window.map(|(group, id)| WindowKey { group, id }) + } + + pub fn retain_visible_windows<'a>(&mut self, items: impl Iterator) { + let visible_windows: HashSet<_> = items.filter_map(Self::window_key).collect(); + + self.cache + .retain(|window_key, _| visible_windows.contains(window_key)); + } +} diff --git a/src/thumbnails/view.rs b/src/thumbnails/view.rs new file mode 100644 index 0000000..eea491c --- /dev/null +++ b/src/thumbnails/view.rs @@ -0,0 +1,78 @@ +use cosmic::Element; +use cosmic::iced::{Border, Color, Length, Shadow, widget::image}; +use cosmic::theme::Container; +use cosmic::widget::space::horizontal as horizontal_space; +use cosmic::widget::{container, row, text}; + +use crate::thumbnails::model::{ThumbnailImage, ThumbnailImageSource}; +use crate::thumbnails::{THUMBNAIL_HEIGHT, THUMBNAIL_WIDTH, ThumbnailState}; + +pub fn thumbnail_placeholder<'a, Message: 'a>() -> Element<'a, Message> { + container(horizontal_space().width(Length::Fill)) + .width(Length::Fixed(THUMBNAIL_WIDTH as f32)) + .height(Length::Fixed(THUMBNAIL_HEIGHT as f32)) + .class(Container::Custom(Box::new(|theme| { + let t = theme.cosmic(); + + container::Style { + background: Some(Color::from_rgba(0.2, 0.2, 0.2, 0.8).into()), + border: Border { + radius: t.radius_s().into(), + width: 1.0, + color: t.bg_divider().into(), + }, + text_color: None, + icon_color: None, + shadow: Shadow::default(), + snap: true, + } + }))) + .into() +} + +pub fn thumbnail_loading<'a, Message: 'a>() -> Element<'a, Message> { + container(row![text::caption("Loading...")].align_y(cosmic::iced::Alignment::Center)) + .width(Length::Fixed(THUMBNAIL_WIDTH as f32)) + .height(Length::Fixed(THUMBNAIL_HEIGHT as f32)) + .center_x(Length::Fill) + .center_y(Length::Fill) + .class(Container::Custom(Box::new(|theme| { + let t = theme.cosmic(); + + container::Style { + background: Some(Color::from_rgba(0.8, 0.5, 0.1, 0.8).into()), + border: Border { + radius: t.radius_s().into(), + width: 1.0, + color: t.bg_divider().into(), + }, + text_color: Some(t.on_bg_color().into()), + icon_color: None, + shadow: Shadow::default(), + snap: true, + } + }))) + .into() +} + +pub fn thumbnail_ready<'a, Message: 'a>(thumbnail_image: ThumbnailImage) -> Element<'a, Message> { + match &thumbnail_image.source { + ThumbnailImageSource::ImageHandle(handle) => container( + image(handle.clone()) + .width(Length::Fixed(THUMBNAIL_WIDTH as f32)) + .height(Length::Fixed(THUMBNAIL_HEIGHT as f32)), + ) + .width(Length::Fixed(THUMBNAIL_WIDTH as f32)) + .height(Length::Fixed(THUMBNAIL_HEIGHT as f32)) + .into(), + } +} + +pub fn thumbnail_view<'a, Message: 'a>(state: ThumbnailState) -> Option> { + match state { + ThumbnailState::Placeholder => Some(thumbnail_placeholder()), + ThumbnailState::Loading => Some(thumbnail_loading()), + ThumbnailState::Ready(image) => Some(thumbnail_ready(image)), + ThumbnailState::Unavailable => Some(thumbnail_placeholder()), + } +}