Dynamically load thumbnails based on what files are visible

This commit is contained in:
Jeremy Soller 2024-02-29 12:26:45 -07:00
parent 5c8fcd4f2e
commit 27b29e9fd8
No known key found for this signature in database
GPG key ID: D02FD439211AF56F
3 changed files with 120 additions and 94 deletions

View file

@ -11,7 +11,7 @@ use cosmic::{
subscription::{self, Subscription}, subscription::{self, Subscription},
window, Alignment, Event, Length, window, Alignment, Event, Length,
}, },
style, style, theme,
widget::{self, segmented_button}, widget::{self, segmented_button},
Application, ApplicationExt, Element, Application, ApplicationExt, Element,
}; };
@ -352,7 +352,7 @@ impl App {
} }
fn about(&self) -> Element<Message> { fn about(&self) -> Element<Message> {
let cosmic_theme::Spacing { space_xxs, .. } = self.core().system_theme().cosmic().spacing; let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
let repository = "https://github.com/pop-os/cosmic-files"; let repository = "https://github.com/pop-os/cosmic-files";
let hash = env!("VERGEN_GIT_SHA"); let hash = env!("VERGEN_GIT_SHA");
let short_hash: String = hash.chars().take(7).collect(); let short_hash: String = hash.chars().take(7).collect();
@ -431,7 +431,7 @@ impl App {
if let Some(ref items) = tab.items_opt { if let Some(ref items) = tab.items_opt {
for item in items.iter() { for item in items.iter() {
if item.selected { if item.selected {
children.push(item.property_view(&self.core, tab.config.icon_sizes)); children.push(item.property_view(tab.config.icon_sizes));
// Only show one property view to avoid issues like hangs when generating // Only show one property view to avoid issues like hangs when generating
// preview images on thousands of files // preview images on thousands of files
break; break;
@ -1146,7 +1146,7 @@ impl Application for App {
None => return None, None => return None,
}; };
let cosmic_theme::Spacing { space_xxs, .. } = self.core().system_theme().cosmic().spacing; let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
let dialog = match dialog_page { let dialog = match dialog_page {
DialogPage::FailedOperation(id) => { DialogPage::FailedOperation(id) => {
@ -1320,7 +1320,7 @@ impl Application for App {
/// Creates a view after each update. /// Creates a view after each update.
fn view(&self) -> Element<Self::Message> { fn view(&self) -> Element<Self::Message> {
let cosmic_theme::Spacing { space_xxs, .. } = self.core().system_theme().cosmic().spacing; let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
let mut tab_column = widget::column::with_capacity(1); let mut tab_column = widget::column::with_capacity(1);
@ -1342,7 +1342,7 @@ impl Application for App {
match self.tab_model.data::<Tab>(entity) { match self.tab_model.data::<Tab>(entity) {
Some(tab) => { Some(tab) => {
let tab_view = tab let tab_view = tab
.view(self.core(), &self.key_binds) .view(&self.key_binds)
.map(move |message| Message::TabMessage(Some(entity), message)); .map(move |message| Message::TabMessage(Some(entity), message));
tab_column = tab_column.push(tab_view); tab_column = tab_column.push(tab_view);
} }

View file

@ -12,6 +12,7 @@ use cosmic::{
subscription::{self, Subscription}, subscription::{self, Subscription},
window, Event, Length, Size, window, Event, Length, Size,
}, },
theme,
widget::{self, segmented_button}, widget::{self, segmented_button},
Application, ApplicationExt, Element, Application, ApplicationExt, Element,
}; };
@ -616,13 +617,13 @@ impl Application for App {
/// Creates a view after each update. /// Creates a view after each update.
fn view(&self) -> Element<Message> { fn view(&self) -> Element<Message> {
let cosmic_theme::Spacing { space_xxs, .. } = self.core().system_theme().cosmic().spacing; let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
let mut tab_column = widget::column::with_capacity(2); let mut tab_column = widget::column::with_capacity(2);
tab_column = tab_column.push( tab_column = tab_column.push(
//TODO: key binds for dialog //TODO: key binds for dialog
self.tab self.tab
.view(self.core(), &HashMap::new()) .view(&HashMap::new())
.map(move |message| Message::TabMessage(message)), .map(move |message| Message::TabMessage(message)),
); );

View file

@ -1,5 +1,4 @@
use cosmic::{ use cosmic::{
app::Core,
cosmic_theme, cosmic_theme,
iced::{ iced::{
alignment::{Horizontal, Vertical}, alignment::{Horizontal, Vertical},
@ -444,8 +443,8 @@ pub struct Item {
} }
impl Item { impl Item {
pub fn property_view(&self, core: &Core, sizes: IconSizes) -> Element<crate::app::Message> { pub fn property_view(&self, sizes: IconSizes) -> Element<crate::app::Message> {
let cosmic_theme::Spacing { space_xxxs, .. } = core.system_theme().cosmic().spacing; let cosmic_theme::Spacing { space_xxxs, .. } = theme::active().cosmic().spacing;
let mut column = widget::column().spacing(space_xxxs); let mut column = widget::column().spacing(space_xxxs);
@ -874,7 +873,7 @@ impl Tab {
commands commands
} }
fn column_sort(&self) -> Option<Vec<Item>> { fn column_sort(&self) -> Option<Vec<&Item>> {
let check_reverse = |ord: Ordering, sort: bool| { let check_reverse = |ord: Ordering, sort: bool| {
if sort { if sort {
ord ord
@ -882,11 +881,11 @@ impl Tab {
ord.reverse() ord.reverse()
} }
}; };
let mut item = self.items_opt.clone()?; let mut items: Vec<&Item> = self.items_opt.as_ref()?.iter().collect();
let heading_sort = self.sort_direction; let heading_sort = self.sort_direction;
match self.sort_name { match self.sort_name {
HeadingOptions::Size => { HeadingOptions::Size => {
item.sort_by(|a, b| { items.sort_by(|a, b| {
// entries take precedence over size // entries take precedence over size
let get_size = |x: &Item| match &x.metadata { let get_size = |x: &Item| match &x.metadata {
ItemMetadata::Path { metadata, children } => { ItemMetadata::Path { metadata, children } => {
@ -912,7 +911,7 @@ impl Tab {
check_reverse(ord, heading_sort) check_reverse(ord, heading_sort)
}) })
} }
HeadingOptions::Name => item.sort_by(|a, b| { HeadingOptions::Name => items.sort_by(|a, b| {
let ord = match (a.path.is_dir(), b.path.is_dir()) { let ord = match (a.path.is_dir(), b.path.is_dir()) {
(true, false) => Ordering::Less, (true, false) => Ordering::Less,
(false, true) => Ordering::Greater, (false, true) => Ordering::Greater,
@ -921,7 +920,7 @@ impl Tab {
check_reverse(ord, heading_sort) check_reverse(ord, heading_sort)
}), }),
HeadingOptions::Modified => { HeadingOptions::Modified => {
item.sort_by(|a, b| { items.sort_by(|a, b| {
let get_modified = |x: &Item| match &x.metadata { let get_modified = |x: &Item| match &x.metadata {
ItemMetadata::Path { metadata, .. } => metadata.modified().ok(), ItemMetadata::Path { metadata, .. } => metadata.modified().ok(),
ItemMetadata::Trash { .. } => None, ItemMetadata::Trash { .. } => None,
@ -933,16 +932,16 @@ impl Tab {
}); });
} }
} }
Some(item) Some(items)
} }
pub fn location_view(&self, core: &Core) -> Element<Message> { pub fn location_view(&self) -> Element<Message> {
let cosmic_theme::Spacing { let cosmic_theme::Spacing {
space_xxxs, space_xxxs,
space_xxs, space_xxs,
space_s, space_s,
.. ..
} = core.system_theme().cosmic().spacing; } = theme::active().cosmic().spacing;
let mut row = widget::row::with_capacity(5).align_items(Alignment::Center); let mut row = widget::row::with_capacity(5).align_items(Alignment::Center);
@ -1080,8 +1079,8 @@ impl Tab {
row.into() row.into()
} }
pub fn empty_view(&self, has_hidden: bool, core: &Core) -> Element<Message> { pub fn empty_view(&self, has_hidden: bool) -> Element<Message> {
let cosmic_theme::Spacing { space_xxs, .. } = core.system_theme().cosmic().spacing; let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
widget::column::with_children(vec![widget::container( widget::column::with_children(vec![widget::container(
widget::column::with_children(vec![ widget::column::with_children(vec![
@ -1107,13 +1106,13 @@ impl Tab {
.into() .into()
} }
pub fn grid_view(&self, core: &Core) -> Element<Message> { pub fn grid_view(&self) -> Element<Message> {
let cosmic_theme::Spacing { let cosmic_theme::Spacing {
space_xs, space_xs,
space_xxs, space_xxs,
space_xxxs, space_xxxs,
.. ..
} = core.system_theme().cosmic().spacing; } = theme::active().cosmic().spacing;
let TabConfig { let TabConfig {
show_hidden, show_hidden,
@ -1214,7 +1213,7 @@ impl Tab {
} }
if count == 0 { if count == 0 {
return self.empty_view(hidden > 0, core); return self.empty_view(hidden > 0);
} }
//TODO: HACK If we don't reach the bottom of the view, go ahead and add a spacer to do that //TODO: HACK If we don't reach the bottom of the view, go ahead and add a spacer to do that
@ -1250,10 +1249,12 @@ impl Tab {
.into() .into()
} }
pub fn list_view(&self, core: &Core) -> Element<Message> { pub fn list_view(&self) -> Element<Message> {
let cosmic_theme::Spacing { space_xxs, .. } = core.system_theme().cosmic().spacing; let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
//TODO: make adaptive? //TODO: make adaptive?
let size = self.size_opt.unwrap_or_else(|| Size::new(0.0, 0.0));
let row_height = 40;
let modified_width = Length::Fixed(200.0); let modified_width = Length::Fixed(200.0);
let size_width = Length::Fixed(100.0); let size_width = Length::Fixed(100.0);
@ -1288,12 +1289,15 @@ impl Tab {
heading_item!("size", size_width, HeadingOptions::Size), heading_item!("size", size_width, HeadingOptions::Size),
]) ])
.align_items(Alignment::Center) .align_items(Alignment::Center)
.height(Length::Fixed(row_height as f32))
.padding(space_xxs) .padding(space_xxs)
.spacing(space_xxs) .spacing(space_xxs)
.into(), .into(),
); );
let mut y = row_height;
children.push(horizontal_rule(1).into()); children.push(horizontal_rule(1).into());
y += 1;
if let Some(ref items) = self.column_sort() { if let Some(ref items) = self.column_sort() {
let mut count = 0; let mut count = 0;
@ -1308,11 +1312,14 @@ impl Tab {
hidden += 1; hidden += 1;
continue; continue;
} }
//TODO: correct rectangle item.rect_opt.set(Some(Rectangle::new(
item.rect_opt.set(None); Point::new(0.0, y as f32),
Size::new(size.width, row_height as f32),
)));
if count > 0 { if count > 0 {
children.push(horizontal_rule(1).into()); children.push(horizontal_rule(1).into());
y += 1;
} }
let modified_text = match &item.metadata { let modified_text = match &item.metadata {
@ -1360,6 +1367,7 @@ impl Tab {
.align_items(Alignment::Center) .align_items(Alignment::Center)
.spacing(space_xxs), .spacing(space_xxs),
) )
.height(Length::Fixed(row_height as f32))
.padding(space_xxs) .padding(space_xxs)
.style(button_style(item.selected, false)) .style(button_style(item.selected, false))
.on_press(Message::Click(Some(i))); .on_press(Message::Click(Some(i)));
@ -1373,10 +1381,11 @@ impl Tab {
); );
} }
count += 1; count += 1;
y += row_height;
} }
if count == 0 { if count == 0 {
return self.empty_view(hidden > 0, core); return self.empty_view(hidden > 0);
} }
} }
@ -1390,11 +1399,11 @@ impl Tab {
.into() .into()
} }
pub fn view(&self, core: &Core, key_binds: &HashMap<KeyBind, Action>) -> Element<Message> { pub fn view(&self, key_binds: &HashMap<KeyBind, Action>) -> Element<Message> {
let location_view = self.location_view(core); let location_view = self.location_view();
let item_view = match self.view { let item_view = match self.view {
View::Grid => self.grid_view(core), View::Grid => self.grid_view(),
View::List => self.list_view(core), View::List => self.list_view(),
}; };
let mut mouse_area = let mut mouse_area =
mouse_area::MouseArea::new(widget::container(item_view).height(Length::Fill)) mouse_area::MouseArea::new(widget::container(item_view).height(Length::Fill))
@ -1428,72 +1437,88 @@ impl Tab {
//TODO: how many thumbnail loads should be in flight at once? //TODO: how many thumbnail loads should be in flight at once?
let jobs = 8; let jobs = 8;
let mut subscriptions = Vec::with_capacity(jobs); let mut subscriptions = Vec::with_capacity(jobs);
let visible_rect = {
let point = match self.scroll_opt {
Some(viewport) => Point::new(0.0, viewport.absolute_offset().y),
None => Point::new(0.0, 0.0),
};
let size = self.size_opt.unwrap_or_else(|| Size::new(0.0, 0.0));
Rectangle::new(point, size)
};
//TODO: HACK to ensure positions are up to date since subscription runs before view
let _ = match self.view {
View::Grid => self.grid_view(),
View::List => self.list_view(),
};
for item in items.iter() { for item in items.iter() {
match item.thumbnail_res_opt { if item.thumbnail_res_opt.is_some() {
Some(_) => continue, // Skip items that already have a thumbnail
continue;
}
match item.rect_opt.get() {
Some(rect) => {
if !rect.intersects(&visible_rect) {
// Skip items that are not visible
continue;
}
}
None => { None => {
let path = item.path.clone(); // Skip items with no determined rect (this should include hidden items)
subscriptions.push(subscription::channel( continue;
path.clone(),
1,
|mut output| async move {
let (path, thumbnail_res) =
tokio::task::spawn_blocking(move || {
let start = std::time::Instant::now();
let thumbnail_res = match image::io::Reader::open(&path) {
Ok(reader) => match reader.decode() {
Ok(image) => {
//TODO: configurable thumbnail size
let thumbnail = image.thumbnail(64, 64);
Ok(thumbnail.to_rgba8())
}
Err(err) => {
log::warn!(
"failed to decode {:?}: {}",
path,
err
);
Err(())
}
},
Err(err) => {
log::warn!("failed to read {:?}: {}", path, err);
Err(())
}
};
log::info!(
"thumbnailed {:?} in {:?}",
path,
start.elapsed()
);
(path, thumbnail_res)
})
.await
.unwrap();
match output
.send(Message::Thumbnail(path.clone(), thumbnail_res))
.await
{
Ok(()) => {}
Err(err) => {
log::warn!(
"failed to send thumbnail for {:?}: {}",
path,
err
);
}
}
//TODO: how to properly kill this task?
loop {
tokio::time::sleep(std::time::Duration::new(1, 0)).await;
}
},
));
} }
} }
let path = item.path.clone();
subscriptions.push(subscription::channel(
path.clone(),
1,
|mut output| async move {
let (path, thumbnail_res) = tokio::task::spawn_blocking(move || {
let start = std::time::Instant::now();
let thumbnail_res = match image::io::Reader::open(&path) {
Ok(reader) => match reader.decode() {
Ok(image) => {
//TODO: configurable thumbnail size
let thumbnail = image.thumbnail(64, 64);
Ok(thumbnail.to_rgba8())
}
Err(err) => {
log::warn!("failed to decode {:?}: {}", path, err);
Err(())
}
},
Err(err) => {
log::warn!("failed to read {:?}: {}", path, err);
Err(())
}
};
log::info!("thumbnailed {:?} in {:?}", path, start.elapsed());
(path, thumbnail_res)
})
.await
.unwrap();
match output
.send(Message::Thumbnail(path.clone(), thumbnail_res))
.await
{
Ok(()) => {}
Err(err) => {
log::warn!("failed to send thumbnail for {:?}: {}", path, err);
}
}
//TODO: how to properly kill this task?
loop {
tokio::time::sleep(std::time::Duration::new(1, 0)).await;
}
},
));
if subscriptions.len() >= jobs { if subscriptions.len() >= jobs {
break; break;
} }