use std::cmp::{max, min};
use std::collections::HashMap;
use std::path::PathBuf;
use std::process::exit;
use freedesktop_desktop_entry::DesktopEntry;
use freedesktop_icons::lookup;
use iced::keyboard::key::Named;
use iced::keyboard::Key;
use iced::widget::button::{primary, text as text_style};
use iced::widget::image::Handle as ImageHandle;
use iced::widget::operation::focus;
use iced::widget::svg::Handle as SvgHandle;
use iced::widget::{button, column, image, row, scrollable, svg, text, text_input, Column};
use iced::{event, window, Alignment, Element, Event, Length, Pixels, Task, Theme};
use iced_layershell::to_layer_message;
use serde::{Deserialize, Serialize};
use crate::values::*;
use crate::CACHE;
use crate::PROGRAM_NAME;
#[cfg(test)]
use iced_runtime::{task::into_stream, Action, Task as RuntimeTask};
fn persist_cache_snapshot(apps: &[AppDescriptor]) {
if let Ok(mut cache) = CACHE.lock() {
if let Err(e) = cache.store_snapshot(apps) {
eprintln!("Failed to persist cache snapshot: {e}");
}
}
}
fn not_loaded_icon() -> IconHandle {
IconHandle::NotLoaded
}
fn icon_handle_from_path(p: PathBuf) -> IconHandle {
if p.extension().and_then(|s| s.to_str()) == Some("svg") {
IconHandle::Vector(SvgHandle::from_path(p))
} else {
IconHandle::Raster(ImageHandle::from_path(p))
}
}
fn default_icon_handle() -> IconHandle {
FALLBACK_ICON_HANDLE.clone()
}
fn set_icon(app: &mut AppDescriptor, handle: IconHandle, path: Option<PathBuf>) {
app.icon_handle = handle;
app.icon_path = path;
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AppDescriptor {
pub appid: String,
pub title: String,
#[serde(default)]
pub lower_title: String,
pub exec: String,
pub exec_count: usize,
pub icon_name: Option<String>,
#[serde(default)]
pub icon_path: Option<PathBuf>,
#[serde(skip, default = "not_loaded_icon")]
pub icon_handle: IconHandle,
}
impl From<DesktopEntry> for AppDescriptor {
fn from(value: DesktopEntry) -> Self {
AppDescriptor {
appid: value.appid.clone(),
title: value.desktop_entry("Name").expect("get name").to_string(),
lower_title: value
.desktop_entry("Name")
.expect("get name")
.to_lowercase(),
exec: value.exec().expect("has exec").to_string(),
exec_count: 0,
icon_name: value.icon().map(str::to_string),
icon_path: None,
icon_handle: IconHandle::NotLoaded,
}
}
}
#[derive(Debug)]
pub struct State {
entry: String,
entry_lower: String,
apps: Vec<AppDescriptor>,
filtered_indices: Vec<usize>,
selected_index: usize,
received_focus: bool,
icon_cache: HashMap<String, IconHandle>,
}
#[derive(Debug)]
pub struct Elbey {
state: State,
flags: ElbeyFlags,
}
const PREFETCH_ICON_COUNT: usize = VIEWABLE_LIST_ITEM_COUNT;
#[to_layer_message]
#[derive(Debug, Clone)]
pub enum ElbeyMessage {
ModelLoaded(Vec<AppDescriptor>),
IconLoaded(usize, Option<PathBuf>),
EntryUpdate(String),
ExecuteSelected(),
KeyEvent(Key),
GainedFocus,
LostFocus,
}
#[derive(Debug, Clone)]
pub struct ElbeyFlags {
pub apps_loader: fn() -> Vec<AppDescriptor>,
pub app_launcher: fn(&AppDescriptor) -> anyhow::Result<()>,
pub theme: Theme,
pub icon_size: u16,
pub hint: String,
pub filter_font_size: u16,
pub entries_font_size: u16,
}
impl Elbey {
pub fn new(flags: ElbeyFlags) -> (Self, Task<ElbeyMessage>) {
let apps_loader = flags.apps_loader;
let load_task = Task::perform(async move { (apps_loader)() }, ElbeyMessage::ModelLoaded);
(
Self {
state: State {
entry: String::new(),
entry_lower: String::new(),
apps: vec![],
filtered_indices: vec![],
selected_index: 0,
received_focus: false,
icon_cache: HashMap::new(),
},
flags,
},
load_task,
)
}
pub fn namespace() -> String {
PROGRAM_NAME.to_string()
}
pub fn view(&self) -> Element<'_, ElbeyMessage> {
let app_elements: Vec<Element<ElbeyMessage>> = self
.state
.filtered_indices
.iter()
.enumerate()
.filter_map(|(filtered_index, original_index)| {
self.state
.apps
.get(*original_index)
.map(|entry| (filtered_index, entry))
})
.filter(|(filtered_index, _)| {
(self.state.selected_index..self.state.selected_index + VIEWABLE_LIST_ITEM_COUNT)
.contains(filtered_index)
}) .map(|(filtered_index, entry)| {
let name = entry.title.as_str();
let selected = self.state.selected_index == filtered_index;
let icon_handle_to_render = match &entry.icon_handle {
IconHandle::NotLoaded => default_icon_handle(),
IconHandle::Loading => default_icon_handle(),
other => other.clone(),
};
let icon: Element<'_, ElbeyMessage> = match icon_handle_to_render {
IconHandle::Raster(handle) => image(handle)
.width(Length::Fixed(self.flags.icon_size.into()))
.height(Length::Fixed(self.flags.icon_size.into()))
.into(),
IconHandle::Vector(handle) => svg(handle)
.width(Length::Fixed(self.flags.icon_size.into()))
.height(Length::Fixed(self.flags.icon_size.into()))
.into(),
IconHandle::Loading => unreachable!(),
IconHandle::NotLoaded => unreachable!(),
};
let content = row![
icon,
text(name).size(Pixels::from(u32::from(self.flags.entries_font_size)))
]
.spacing(10)
.align_y(Alignment::Center);
button(content)
.style(if selected { primary } else { text_style })
.width(Length::Fill)
.on_press(ElbeyMessage::ExecuteSelected())
.into()
})
.collect();
column![
text_input(&self.flags.hint, &self.state.entry)
.id(ENTRY_WIDGET_ID.clone())
.on_input(ElbeyMessage::EntryUpdate)
.size(Pixels::from(u32::from(self.flags.filter_font_size)))
.width(Length::Fill),
scrollable(Column::with_children(app_elements))
.width(Length::Fill)
.id(ITEMS_WIDGET_ID.clone()),
]
.into()
}
pub fn update(&mut self, message: ElbeyMessage) -> Task<ElbeyMessage> {
match message {
ElbeyMessage::ModelLoaded(items) => {
self.state.apps = items;
self.state.entry_lower = self.state.entry.to_lowercase();
self.state.icon_cache.reserve(
self.state
.apps
.len()
.saturating_sub(self.state.icon_cache.len()),
);
self.refresh_filtered_indices();
let focus_task = focus(ENTRY_WIDGET_ID.clone());
let load_icons_task = self.load_visible_icons();
Task::batch(vec![focus_task, load_icons_task])
}
ElbeyMessage::EntryUpdate(entry_text) => {
self.state.entry = entry_text;
self.state.entry_lower = self.state.entry.to_lowercase();
self.state.selected_index = 0;
self.refresh_filtered_indices();
self.load_visible_icons()
}
ElbeyMessage::ExecuteSelected() => {
if let Some(entry) = self.selected_entry() {
(self.flags.app_launcher)(entry).expect("Failed to launch app");
}
Task::none()
}
ElbeyMessage::IconLoaded(index, path) => {
if let Some(app) = self.state.apps.get_mut(index) {
if let Some(p) = path {
let handle = icon_handle_from_path(p.clone());
if let Some(icon_name) = app.icon_name.clone() {
self.state.icon_cache.insert(icon_name, handle.clone());
}
set_icon(app, handle, Some(p));
} else {
let fallback = default_icon_handle();
if let Some(icon_name) = app.icon_name.clone() {
self.state.icon_cache.insert(icon_name, fallback.clone());
}
set_icon(app, fallback, Some(PathBuf::new()));
}
persist_cache_snapshot(&self.state.apps);
}
Task::none()
}
ElbeyMessage::KeyEvent(key) => match key {
Key::Named(Named::Escape) => {
persist_cache_snapshot(&self.state.apps);
exit(0)
}
Key::Named(Named::ArrowUp) => {
self.navigate_items(-1);
self.load_visible_icons()
}
Key::Named(Named::ArrowDown) => {
self.navigate_items(1);
self.load_visible_icons()
}
Key::Named(Named::PageUp) => {
self.navigate_items(-(VIEWABLE_LIST_ITEM_COUNT as i32));
self.load_visible_icons()
}
Key::Named(Named::PageDown) => {
self.navigate_items(VIEWABLE_LIST_ITEM_COUNT as i32);
self.load_visible_icons()
}
Key::Named(Named::Enter) => {
if let Some(entry) = self.selected_entry() {
(self.flags.app_launcher)(entry).expect("Failed to launch app");
}
Task::none()
}
_ => Task::none(),
},
ElbeyMessage::GainedFocus => {
self.state.received_focus = true;
focus(ENTRY_WIDGET_ID.clone())
}
ElbeyMessage::LostFocus => {
if self.state.received_focus {
persist_cache_snapshot(&self.state.apps);
exit(0);
}
Task::none()
}
ElbeyMessage::AnchorChange(anchor) => {
dbg!(anchor);
Task::none()
}
ElbeyMessage::SetInputRegion(_action_callback) => Task::none(),
ElbeyMessage::AnchorSizeChange(anchor, _) => {
dbg!(anchor);
Task::none()
}
ElbeyMessage::ExclusiveZoneChange(exclusive_zone) => {
dbg!(exclusive_zone);
Task::none()
}
ElbeyMessage::LayerChange(layer) => {
dbg!(layer);
Task::none()
}
ElbeyMessage::MarginChange(mc) => {
dbg!(mc);
Task::none()
}
ElbeyMessage::SizeChange(sc) => {
dbg!(sc);
Task::none()
}
ElbeyMessage::VirtualKeyboardPressed { time, key } => {
dbg!(time, key);
Task::none()
}
}
}
pub fn subscription(&self) -> iced::Subscription<ElbeyMessage> {
event::listen_with(|event, _status, _| match event {
Event::Window(window::Event::Focused) => Some(ElbeyMessage::GainedFocus),
Event::Window(window::Event::Unfocused) => Some(ElbeyMessage::LostFocus),
Event::Keyboard(iced::keyboard::Event::KeyPressed {
modifiers: _,
text: _,
key,
location: _,
modified_key: _,
physical_key: _,
repeat: _,
}) => Some(ElbeyMessage::KeyEvent(key)),
_ => None,
})
}
pub fn theme(&self) -> Theme {
self.flags.theme.clone()
}
}
impl Elbey {
fn selected_entry(&self) -> Option<&AppDescriptor> {
self.state
.filtered_indices
.get(self.state.selected_index)
.and_then(|original_index| self.state.apps.get(*original_index))
}
fn navigate_items(&mut self, delta: i32) {
let filtered_len = self.state.filtered_indices.len();
if filtered_len == 0 {
self.state.selected_index = 0;
return;
}
if delta < 0 {
self.state.selected_index = max(0, self.state.selected_index as i32 + delta) as usize;
} else {
self.state.selected_index = min(
filtered_len as i32 - 1,
self.state.selected_index as i32 + delta,
) as usize;
}
}
fn text_entry_filter(entry: &AppDescriptor, model: &State) -> bool {
entry.lower_title.contains(&model.entry_lower)
}
fn queue_icon_load(
&mut self,
original_index: usize,
icon_size: u16,
tasks: &mut Vec<Task<ElbeyMessage>>,
) {
if let Some(app) = self.state.apps.get_mut(original_index) {
if let Some(icon_name) = app.icon_name.clone() {
if let Some(icon_path) = app.icon_path.clone() {
if icon_path.as_os_str().is_empty() {
set_icon(app, default_icon_handle(), Some(icon_path));
return;
}
if matches!(app.icon_handle, IconHandle::NotLoaded) {
let handle = icon_handle_from_path(icon_path);
self.state
.icon_cache
.insert(icon_name.clone(), handle.clone());
set_icon(app, handle, app.icon_path.clone());
return;
}
}
if let Some(cached) = self.state.icon_cache.get(&icon_name) {
app.icon_handle = cached.clone();
return;
}
if app.icon_handle == IconHandle::Loading {
return;
}
if matches!(app.icon_handle, IconHandle::NotLoaded) {
app.icon_handle = IconHandle::Loading;
tasks.push(Task::perform(
async move { lookup(&icon_name).with_size(icon_size).with_cache().find() },
move |path| ElbeyMessage::IconLoaded(original_index, path),
));
}
}
}
}
fn load_visible_icons(&mut self) -> Task<ElbeyMessage> {
let filtered_app_indices = self.state.filtered_indices.clone();
let view_start = self.state.selected_index;
let view_end =
(self.state.selected_index + VIEWABLE_LIST_ITEM_COUNT).min(filtered_app_indices.len());
let icon_size = self.flags.icon_size;
let mut tasks = vec![];
if let Some(visible_indices) = filtered_app_indices.get(view_start..view_end) {
for &original_index in visible_indices {
self.queue_icon_load(original_index, icon_size, &mut tasks);
}
}
let prefetch_end = (view_end + PREFETCH_ICON_COUNT).min(filtered_app_indices.len());
if let Some(prefetch_indices) = filtered_app_indices.get(view_end..prefetch_end) {
for &original_index in prefetch_indices {
self.queue_icon_load(original_index, icon_size, &mut tasks);
}
}
Task::batch(tasks)
}
#[cfg(test)]
fn load_all_icons(&mut self) -> Task<ElbeyMessage> {
let icon_size = self.flags.icon_size;
let mut tasks = vec![];
for idx in 0..self.state.apps.len() {
self.queue_icon_load(idx, icon_size, &mut tasks);
}
Task::batch(tasks)
}
fn refresh_filtered_indices(&mut self) {
self.state.filtered_indices = self
.state
.apps
.iter()
.enumerate()
.filter(|(_, e)| Self::text_entry_filter(e, &self.state))
.map(|(i, _)| i)
.collect();
if self.state.selected_index >= self.state.filtered_indices.len() {
self.state.selected_index = self.state.filtered_indices.len().saturating_sub(1);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use iced::futures::stream::StreamExt;
use std::sync::{LazyLock, OnceLock};
use std::time::Instant;
fn set_test_cache_home() {
static CACHE_HOME: OnceLock<PathBuf> = OnceLock::new();
let cache_dir = CACHE_HOME.get_or_init(|| {
let mut dir = std::env::temp_dir();
dir.push(format!("elbey-test-cache-{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
dir
});
std::env::set_var("XDG_CACHE_HOME", cache_dir);
}
static EMPTY_LOADER: fn() -> Vec<AppDescriptor> = || vec![];
static TEST_DESKTOP_ENTRY_1: LazyLock<AppDescriptor> = LazyLock::new(|| AppDescriptor {
appid: "test_app_id_1".to_string(),
title: "t1".to_string(),
lower_title: "t1".to_string(),
exec: "".to_string(),
exec_count: 0,
icon_name: None,
icon_path: None,
icon_handle: IconHandle::NotLoaded,
});
static TEST_DESKTOP_ENTRY_2: LazyLock<AppDescriptor> = LazyLock::new(|| AppDescriptor {
appid: "test_app_id_2".to_string(),
title: "t2".to_string(),
lower_title: "t2".to_string(),
exec: "".to_string(),
exec_count: 0,
icon_name: None,
icon_path: None,
icon_handle: IconHandle::NotLoaded,
});
static TEST_DESKTOP_ENTRY_3: LazyLock<AppDescriptor> = LazyLock::new(|| AppDescriptor {
appid: "test_app_id_3".to_string(),
title: "t2".to_string(),
lower_title: "t2".to_string(),
exec: "".to_string(),
exec_count: 0,
icon_name: None,
icon_path: None,
icon_handle: IconHandle::NotLoaded,
});
static TEST_ENTRY_LOADER: fn() -> Vec<AppDescriptor> = || {
vec![
TEST_DESKTOP_ENTRY_1.clone(),
TEST_DESKTOP_ENTRY_2.clone(),
TEST_DESKTOP_ENTRY_3.clone(),
]
};
#[test]
fn test_default_app_launch() {
let test_launcher: fn(&AppDescriptor) -> anyhow::Result<()> = |e| {
assert!(e.appid == "test_app_id_1");
Ok(())
};
let (mut unit, _) = Elbey::new(ElbeyFlags {
apps_loader: TEST_ENTRY_LOADER,
app_launcher: test_launcher,
theme: DEFAULT_THEME,
icon_size: 48,
hint: DEFAULT_HINT.to_string(),
filter_font_size: DEFAULT_TEXT_SIZE,
entries_font_size: DEFAULT_TEXT_SIZE,
});
let _ = unit.update(ElbeyMessage::ModelLoaded(TEST_ENTRY_LOADER()));
let _ = unit.update(ElbeyMessage::ExecuteSelected());
}
#[test]
fn test_no_apps_try_launch() {
let test_launcher: fn(&AppDescriptor) -> anyhow::Result<()> = |_e| {
assert!(false); Ok(())
};
let (mut unit, _) = Elbey::new(ElbeyFlags {
apps_loader: TEST_ENTRY_LOADER,
app_launcher: test_launcher,
theme: DEFAULT_THEME,
icon_size: 48,
hint: DEFAULT_HINT.to_string(),
filter_font_size: DEFAULT_TEXT_SIZE,
entries_font_size: DEFAULT_TEXT_SIZE,
});
let _ = unit.update(ElbeyMessage::ModelLoaded(EMPTY_LOADER()));
let _result = unit.update(ElbeyMessage::ExecuteSelected());
}
#[test]
fn test_app_navigation() {
let test_launcher: fn(&AppDescriptor) -> anyhow::Result<()> = |e| {
assert!(e.appid == "test_app_id_2");
Ok(())
};
let (mut unit, _) = Elbey::new(ElbeyFlags {
apps_loader: TEST_ENTRY_LOADER,
app_launcher: test_launcher,
theme: DEFAULT_THEME,
icon_size: 48,
hint: DEFAULT_HINT.to_string(),
filter_font_size: DEFAULT_TEXT_SIZE,
entries_font_size: DEFAULT_TEXT_SIZE,
});
let _ = unit.update(ElbeyMessage::ModelLoaded(TEST_ENTRY_LOADER()));
let _ = unit.update(ElbeyMessage::KeyEvent(Key::Named(Named::ArrowDown)));
let _ = unit.update(ElbeyMessage::KeyEvent(Key::Named(Named::ArrowDown)));
let _ = unit.update(ElbeyMessage::KeyEvent(Key::Named(Named::ArrowUp)));
let _ = unit.update(ElbeyMessage::ExecuteSelected());
}
#[test]
fn test_icon_loaded_png() {
set_test_cache_home();
let (mut unit, _) = Elbey::new(ElbeyFlags {
apps_loader: TEST_ENTRY_LOADER,
app_launcher: |_| Ok(()),
theme: DEFAULT_THEME,
icon_size: 48,
hint: DEFAULT_HINT.to_string(),
filter_font_size: DEFAULT_TEXT_SIZE,
entries_font_size: DEFAULT_TEXT_SIZE,
});
let _ = unit.update(ElbeyMessage::ModelLoaded(TEST_ENTRY_LOADER()));
let png_path = PathBuf::from("test.png");
let _ = unit.update(ElbeyMessage::IconLoaded(0, Some(png_path)));
assert!(matches!(
unit.state.apps[0].icon_handle,
IconHandle::Raster(_)
));
}
#[test]
fn test_icon_loaded_svg() {
set_test_cache_home();
let (mut unit, _) = Elbey::new(ElbeyFlags {
apps_loader: TEST_ENTRY_LOADER,
app_launcher: |_| Ok(()),
theme: DEFAULT_THEME,
icon_size: 48,
hint: DEFAULT_HINT.to_string(),
filter_font_size: DEFAULT_TEXT_SIZE,
entries_font_size: DEFAULT_TEXT_SIZE,
});
let _ = unit.update(ElbeyMessage::ModelLoaded(TEST_ENTRY_LOADER()));
let svg_path = PathBuf::from("test.svg");
let _ = unit.update(ElbeyMessage::IconLoaded(0, Some(svg_path)));
assert!(matches!(
unit.state.apps[0].icon_handle,
IconHandle::Vector(_)
));
}
#[test]
fn test_icon_loaded_fallback() {
set_test_cache_home();
let (mut unit, _) = Elbey::new(ElbeyFlags {
apps_loader: TEST_ENTRY_LOADER,
app_launcher: |_| Ok(()),
theme: DEFAULT_THEME,
icon_size: 48,
hint: DEFAULT_HINT.to_string(),
filter_font_size: DEFAULT_TEXT_SIZE,
entries_font_size: DEFAULT_TEXT_SIZE,
});
let _ = unit.update(ElbeyMessage::ModelLoaded(TEST_ENTRY_LOADER()));
let _ = unit.update(ElbeyMessage::IconLoaded(0, None));
assert!(matches!(
unit.state.apps[0].icon_handle,
IconHandle::Vector(_)
));
}
#[test]
#[ignore]
fn measure_load_visible_icons_time() {
let (mut unit, _) = Elbey::new(ElbeyFlags {
apps_loader: EMPTY_LOADER,
app_launcher: |_| Ok(()),
theme: DEFAULT_THEME,
icon_size: 48,
hint: DEFAULT_HINT.to_string(),
filter_font_size: DEFAULT_TEXT_SIZE,
entries_font_size: DEFAULT_TEXT_SIZE,
});
let app_count = 50_000;
unit.state.apps = (0..app_count)
.map(|i| AppDescriptor {
appid: format!("test_app_id_{i}"),
title: format!("App {i}"),
lower_title: format!("app {i}"),
exec: "".to_string(),
exec_count: 0,
icon_name: None,
icon_path: None,
icon_handle: IconHandle::NotLoaded,
})
.collect();
unit.state.entry = "app 4".to_string();
unit.state.entry_lower = unit.state.entry.to_lowercase();
unit.state.selected_index = 0;
let start = Instant::now();
let _ = unit.load_visible_icons();
let elapsed = start.elapsed();
println!(
"load_visible_icons on {app_count} apps took {:?} (view size {})",
elapsed, VIEWABLE_LIST_ITEM_COUNT
);
}
fn drain_tasks(elbey: &mut Elbey, task: RuntimeTask<ElbeyMessage>) {
use std::collections::VecDeque;
let mut queue = VecDeque::new();
queue.push_back(task);
let mut runtime = iced::futures::executor::LocalPool::new();
while let Some(task) = queue.pop_front() {
if let Some(mut stream) = into_stream(task) {
let mut outputs = Vec::new();
runtime.run_until(async {
while let Some(action) = stream.next().await {
if let Action::Output(msg) = action {
outputs.push(msg);
}
}
});
for message in outputs {
let follow_up = elbey.update(message);
queue.push_back(follow_up);
}
}
}
}
#[test]
#[ignore]
fn measure_firefox_icon_latency() {
set_test_cache_home();
let locales = freedesktop_desktop_entry::get_languages_from_env();
let locales_ref: Vec<&str> = locales.iter().map(String::as_str).collect();
let firefox_entry = DesktopEntry::from_path(
PathBuf::from("/usr/share/applications/firefox.desktop"),
Some(&locales_ref),
)
.expect("firefox desktop entry missing");
let firefox = AppDescriptor::from(firefox_entry);
let (mut elbey, _) = Elbey::new(ElbeyFlags {
apps_loader: EMPTY_LOADER,
app_launcher: |_| Ok(()),
theme: DEFAULT_THEME,
icon_size: DEFAULT_ICON_SIZE,
hint: DEFAULT_HINT.to_string(),
filter_font_size: DEFAULT_TEXT_SIZE,
entries_font_size: DEFAULT_TEXT_SIZE,
});
let start = Instant::now();
let initial = elbey.update(ElbeyMessage::ModelLoaded(vec![firefox]));
drain_tasks(&mut elbey, initial);
let elapsed = start.elapsed();
if let Some(app) = elbey.state.apps.first() {
println!(
"firefox icon latency: {:?} (icon_name: {:?})",
elapsed, app.icon_name
);
assert!(app.icon_name.is_some(), "icon did not resolve");
} else {
panic!("no app loaded");
}
assert!(
elapsed.as_millis() > 0,
"latency too small; headless timing likely invalid"
);
}
#[test]
#[ignore]
fn measure_all_icons_latency() {
set_test_cache_home();
use crate::load_apps;
let (mut cold, _) = Elbey::new(ElbeyFlags {
apps_loader: load_apps,
app_launcher: |_| Ok(()),
theme: DEFAULT_THEME,
icon_size: DEFAULT_ICON_SIZE,
hint: DEFAULT_HINT.to_string(),
filter_font_size: DEFAULT_TEXT_SIZE,
entries_font_size: DEFAULT_TEXT_SIZE,
});
let start_cold = Instant::now();
let initial_apps = (cold.flags.apps_loader)();
let task = cold.update(ElbeyMessage::ModelLoaded(initial_apps));
drain_tasks(&mut cold, task);
let all_icons = cold.load_all_icons();
drain_tasks(&mut cold, all_icons);
let cold_elapsed = start_cold.elapsed();
let cold_resolved = cold
.state
.apps
.iter()
.filter(|app| {
matches!(
app.icon_handle,
IconHandle::Vector(_) | IconHandle::Raster(_)
)
})
.count();
println!(
"all-apps icon latency (cold): {:?} across {} apps (resolved: {})",
cold_elapsed,
cold.state.apps.len(),
cold_resolved
);
let (mut warm, _) = Elbey::new(ElbeyFlags {
apps_loader: load_apps,
app_launcher: |_| Ok(()),
theme: DEFAULT_THEME,
icon_size: DEFAULT_ICON_SIZE,
hint: DEFAULT_HINT.to_string(),
filter_font_size: DEFAULT_TEXT_SIZE,
entries_font_size: DEFAULT_TEXT_SIZE,
});
let start_warm = Instant::now();
let warm_apps = (warm.flags.apps_loader)();
let warm_task = warm.update(ElbeyMessage::ModelLoaded(warm_apps));
drain_tasks(&mut warm, warm_task);
let warm_icons = warm.load_all_icons();
drain_tasks(&mut warm, warm_icons);
let warm_elapsed = start_warm.elapsed();
let warm_resolved = warm
.state
.apps
.iter()
.filter(|app| {
matches!(
app.icon_handle,
IconHandle::Vector(_) | IconHandle::Raster(_)
)
})
.count();
println!(
"all-apps icon latency (warm): {:?} across {} apps (resolved: {})",
warm_elapsed,
warm.state.apps.len(),
warm_resolved
);
assert!(
warm_elapsed < cold_elapsed,
"warm run should be faster than cold run"
);
}
}