perf: optimize startup by caching icons, texture, and async avatar

- Replace manual icon theme lookup + Pixbuf scaling with native
  GTK4 Image::from_icon_name() (uses internal cache + GPU rendering)
- Decode wallpaper texture once and share across all windows
  instead of N+1 separate JPEG decodes
- Load file-based avatars asynchronously via gio::spawn_blocking
  to avoid blocking the UI thread
This commit is contained in:
nevaforget 2026-03-28 09:47:47 +01:00
parent d6979c1792
commit b22172c3a0
2 changed files with 59 additions and 56 deletions

View File

@ -51,12 +51,13 @@ fn activate(app: &gtk::Application) {
load_css(&display);
// Resolve wallpaper once, share across all windows
// Resolve wallpaper once, decode texture once, share across all windows
let config = config::load_config(None);
let bg_path = config::resolve_background_path(&config);
let texture = panel::load_background_texture(&bg_path);
// Panel on focused output (no set_monitor → compositor picks focused)
let panel = panel::create_panel_window(&bg_path, app);
let panel = panel::create_panel_window(&texture, app);
setup_layer_shell(&panel, true, gtk4_layer_shell::Layer::Overlay);
panel.present();
@ -64,7 +65,7 @@ fn activate(app: &gtk::Application) {
let monitors = display.monitors();
for i in 0..monitors.n_items() {
if let Some(monitor) = monitors.item(i).and_then(|obj| obj.downcast::<gdk::Monitor>().ok()) {
let wallpaper = panel::create_wallpaper_window(&bg_path, app);
let wallpaper = panel::create_wallpaper_window(&texture, app);
setup_layer_shell(&wallpaper, false, gtk4_layer_shell::Layer::Top);
wallpaper.set_monitor(Some(&monitor));
wallpaper.present();

View File

@ -79,14 +79,26 @@ pub fn action_definitions() -> Vec<ActionDef> {
]
}
/// Load the wallpaper as a texture once, for sharing across all windows.
pub fn load_background_texture(bg_path: &Path) -> gdk::Texture {
if bg_path.starts_with("/dev/moonarch/moonset") {
gdk::Texture::from_resource(bg_path.to_str().unwrap_or(""))
} else {
let file = gio::File::for_path(bg_path);
gdk::Texture::from_file(&file).unwrap_or_else(|_| {
gdk::Texture::from_resource("/dev/moonarch/moonset/wallpaper.jpg")
})
}
}
/// Create a wallpaper-only window for secondary monitors.
pub fn create_wallpaper_window(bg_path: &Path, app: &gtk::Application) -> gtk::ApplicationWindow {
pub fn create_wallpaper_window(texture: &gdk::Texture, app: &gtk::Application) -> gtk::ApplicationWindow {
let window = gtk::ApplicationWindow::builder()
.application(app)
.build();
window.add_css_class("wallpaper");
let background = create_background_picture(bg_path);
let background = create_background_picture(texture);
window.set_child(Some(&background));
// Fade-in on map
@ -104,7 +116,7 @@ pub fn create_wallpaper_window(bg_path: &Path, app: &gtk::Application) -> gtk::A
}
/// Create the main panel window with action buttons and confirm flow.
pub fn create_panel_window(bg_path: &Path, app: &gtk::Application) -> gtk::ApplicationWindow {
pub fn create_panel_window(texture: &gdk::Texture, app: &gtk::Application) -> gtk::ApplicationWindow {
let window = gtk::ApplicationWindow::builder()
.application(app)
.build();
@ -126,7 +138,7 @@ pub fn create_panel_window(bg_path: &Path, app: &gtk::Application) -> gtk::Appli
window.set_child(Some(&overlay));
// Background wallpaper
let background = create_background_picture(bg_path);
let background = create_background_picture(texture);
overlay.set_child(Some(&background));
// Click on background dismisses the menu
@ -157,13 +169,8 @@ pub fn create_panel_window(bg_path: &Path, app: &gtk::Application) -> gtk::Appli
avatar_frame.append(&avatar_image);
content_box.append(&avatar_frame);
// Load avatar
let avatar_path = users::get_avatar_path(&user.home, Some(&user.username));
if let Some(path) = avatar_path {
set_avatar_from_file(&avatar_image, &path);
} else {
set_default_avatar(&avatar_image, &window);
}
// Load avatar (file-based avatars load asynchronously)
load_avatar_async(&avatar_image, &window, &user);
// Username label
let username_label = gtk::Label::new(Some(&user.display_name));
@ -237,13 +244,9 @@ pub fn create_panel_window(bg_path: &Path, app: &gtk::Application) -> gtk::Appli
window
}
/// Create a Picture widget for the wallpaper background.
fn create_background_picture(bg_path: &Path) -> gtk::Picture {
let background = if bg_path.starts_with("/dev/moonarch/moonset") {
gtk::Picture::for_resource(bg_path.to_str().unwrap_or(""))
} else {
gtk::Picture::for_filename(bg_path.to_str().unwrap_or(""))
};
/// Create a Picture widget for the wallpaper background from a shared texture.
fn create_background_picture(texture: &gdk::Texture) -> gtk::Picture {
let background = gtk::Picture::for_paintable(texture);
background.set_content_fit(gtk::ContentFit::Cover);
background.set_hexpand(true);
background.set_vexpand(true);
@ -302,34 +305,9 @@ fn create_action_button(
button
}
/// Load a symbolic icon at 22px and scale to 64px via GdkPixbuf.
/// Load a symbolic icon using native GTK4 rendering at the target size.
fn load_scaled_icon(icon_name: &str) -> gtk::Image {
let display = gdk::Display::default().unwrap();
let theme = gtk::IconTheme::for_display(&display);
let icon_paintable = theme.lookup_icon(
icon_name,
&[],
22,
1,
gtk::TextDirection::None,
gtk::IconLookupFlags::FORCE_SYMBOLIC,
);
let icon = gtk::Image::new();
if let Some(file) = icon_paintable.file() {
if let Some(path) = file.path() {
if let Ok(pixbuf) =
Pixbuf::from_file_at_scale(path.to_str().unwrap_or(""), 64, 64, true)
{
let texture = gdk::Texture::for_pixbuf(&pixbuf);
icon.set_paintable(Some(&texture));
return icon;
}
}
}
// Fallback: use icon name directly
icon.set_icon_name(Some(icon_name));
let icon = gtk::Image::from_icon_name(icon_name);
icon.set_pixel_size(64);
icon
}
@ -479,15 +457,39 @@ fn execute_action(
));
}
/// Load an image file and set it as the avatar.
fn set_avatar_from_file(image: &gtk::Image, path: &Path) {
match Pixbuf::from_file_at_scale(path.to_str().unwrap_or(""), AVATAR_SIZE, AVATAR_SIZE, true) {
Ok(pixbuf) => {
let texture = gdk::Texture::for_pixbuf(&pixbuf);
image.set_paintable(Some(&texture));
/// Load the avatar asynchronously. File-based avatars are decoded off the UI thread.
fn load_avatar_async(image: &gtk::Image, window: &gtk::ApplicationWindow, user: &users::User) {
let avatar_path = users::get_avatar_path(&user.home, Some(&user.username));
match avatar_path {
Some(path) => {
// File-based avatar: load and scale in background thread
glib::spawn_future_local(clone!(
#[weak]
image,
async move {
let result = gio::spawn_blocking(move || {
Pixbuf::from_file_at_scale(
path.to_str().unwrap_or(""),
AVATAR_SIZE,
AVATAR_SIZE,
true,
)
.ok()
.map(|pb| gdk::Texture::for_pixbuf(&pb))
})
.await;
match result {
Ok(Some(texture)) => image.set_paintable(Some(&texture)),
_ => image.set_icon_name(Some("avatar-default-symbolic")),
}
}
));
}
Err(_) => {
image.set_icon_name(Some("avatar-default-symbolic"));
None => {
// Default SVG avatar: needs widget color, keep synchronous
set_default_avatar(image, window);
}
}
}