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); 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 config = config::load_config(None);
let bg_path = config::resolve_background_path(&config); 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) // 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); setup_layer_shell(&panel, true, gtk4_layer_shell::Layer::Overlay);
panel.present(); panel.present();
@ -64,7 +65,7 @@ fn activate(app: &gtk::Application) {
let monitors = display.monitors(); let monitors = display.monitors();
for i in 0..monitors.n_items() { for i in 0..monitors.n_items() {
if let Some(monitor) = monitors.item(i).and_then(|obj| obj.downcast::<gdk::Monitor>().ok()) { 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); setup_layer_shell(&wallpaper, false, gtk4_layer_shell::Layer::Top);
wallpaper.set_monitor(Some(&monitor)); wallpaper.set_monitor(Some(&monitor));
wallpaper.present(); 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. /// 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() let window = gtk::ApplicationWindow::builder()
.application(app) .application(app)
.build(); .build();
window.add_css_class("wallpaper"); window.add_css_class("wallpaper");
let background = create_background_picture(bg_path); let background = create_background_picture(texture);
window.set_child(Some(&background)); window.set_child(Some(&background));
// Fade-in on map // 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. /// 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() let window = gtk::ApplicationWindow::builder()
.application(app) .application(app)
.build(); .build();
@ -126,7 +138,7 @@ pub fn create_panel_window(bg_path: &Path, app: &gtk::Application) -> gtk::Appli
window.set_child(Some(&overlay)); window.set_child(Some(&overlay));
// Background wallpaper // Background wallpaper
let background = create_background_picture(bg_path); let background = create_background_picture(texture);
overlay.set_child(Some(&background)); overlay.set_child(Some(&background));
// Click on background dismisses the menu // 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); avatar_frame.append(&avatar_image);
content_box.append(&avatar_frame); content_box.append(&avatar_frame);
// Load avatar // Load avatar (file-based avatars load asynchronously)
let avatar_path = users::get_avatar_path(&user.home, Some(&user.username)); load_avatar_async(&avatar_image, &window, &user);
if let Some(path) = avatar_path {
set_avatar_from_file(&avatar_image, &path);
} else {
set_default_avatar(&avatar_image, &window);
}
// Username label // Username label
let username_label = gtk::Label::new(Some(&user.display_name)); 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 window
} }
/// Create a Picture widget for the wallpaper background. /// Create a Picture widget for the wallpaper background from a shared texture.
fn create_background_picture(bg_path: &Path) -> gtk::Picture { fn create_background_picture(texture: &gdk::Texture) -> gtk::Picture {
let background = if bg_path.starts_with("/dev/moonarch/moonset") { let background = gtk::Picture::for_paintable(texture);
gtk::Picture::for_resource(bg_path.to_str().unwrap_or(""))
} else {
gtk::Picture::for_filename(bg_path.to_str().unwrap_or(""))
};
background.set_content_fit(gtk::ContentFit::Cover); background.set_content_fit(gtk::ContentFit::Cover);
background.set_hexpand(true); background.set_hexpand(true);
background.set_vexpand(true); background.set_vexpand(true);
@ -302,34 +305,9 @@ fn create_action_button(
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 { fn load_scaled_icon(icon_name: &str) -> gtk::Image {
let display = gdk::Display::default().unwrap(); let icon = gtk::Image::from_icon_name(icon_name);
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));
icon.set_pixel_size(64); icon.set_pixel_size(64);
icon icon
} }
@ -479,15 +457,39 @@ fn execute_action(
)); ));
} }
/// Load an image file and set it as the avatar. /// Load the avatar asynchronously. File-based avatars are decoded off the UI thread.
fn set_avatar_from_file(image: &gtk::Image, path: &Path) { fn load_avatar_async(image: &gtk::Image, window: &gtk::ApplicationWindow, user: &users::User) {
match Pixbuf::from_file_at_scale(path.to_str().unwrap_or(""), AVATAR_SIZE, AVATAR_SIZE, true) { let avatar_path = users::get_avatar_path(&user.home, Some(&user.username));
Ok(pixbuf) => {
let texture = gdk::Texture::for_pixbuf(&pixbuf); match avatar_path {
image.set_paintable(Some(&texture)); 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);
} }
} }
} }