Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion rustfmt.toml
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
imports_granularity = "Module"
35 changes: 35 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -161,6 +164,7 @@ pub struct CosmicLauncher {
needs_clear: bool,
hand_over: String,
dummy_id: Option<window::Id>,
thumbnail_provider: ThumbnailProvider,
}

#[derive(Debug, Clone)]
Expand All @@ -186,6 +190,7 @@ pub enum Message {
AltRelease,
Overlap(OverlapNotifyEvent),
Surface(surface::Action),
ThumbnailLoaded(ThumbnailResult),
}

impl CosmicLauncher {
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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::<Vec<_>>();

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);
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod config;
mod app;
mod localize;
mod subscriptions;
mod thumbnails;
use tracing::info;

use localize::localize;
Expand Down
32 changes: 32 additions & 0 deletions src/thumbnails/loader.rs
Original file line number Diff line number Diff line change
@@ -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<ThumbnailImageSource> {
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,
}
}
9 changes: 9 additions & 0 deletions src/thumbnails/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
45 changes: 45 additions & 0 deletions src/thumbnails/model.rs
Original file line number Diff line number Diff line change
@@ -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<RgbaThumbnailData>,
}

#[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<u8>,
}
123 changes: 123 additions & 0 deletions src/thumbnails/provider.rs
Original file line number Diff line number Diff line change
@@ -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<ThumbnailSignature>,
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<WindowKey, CachedThumbnail>,
}

impl ThumbnailProvider {
pub fn new() -> Self {
Self {
cache: HashMap::new(),
}
}

fn thumbnail_signature(item: &SearchResult) -> Option<ThumbnailSignature> {
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<ThumbnailRequest> {
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<WindowKey> {
item.window.map(|(group, id)| WindowKey { group, id })
}

pub fn retain_visible_windows<'a>(&mut self, items: impl Iterator<Item = &'a SearchResult>) {
let visible_windows: HashSet<_> = items.filter_map(Self::window_key).collect();

self.cache
.retain(|window_key, _| visible_windows.contains(window_key));
}
}
78 changes: 78 additions & 0 deletions src/thumbnails/view.rs
Original file line number Diff line number Diff line change
@@ -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<Element<'a, Message>> {
match state {
ThumbnailState::Placeholder => Some(thumbnail_placeholder()),
ThumbnailState::Loading => Some(thumbnail_loading()),
ThumbnailState::Ready(image) => Some(thumbnail_ready(image)),
ThumbnailState::Unavailable => Some(thumbnail_placeholder()),
}
}