window: Adjust to image size when opening

Give LpImage a native with that should feel nice.
When opening a new window with a new image the window size
is determined by LpImage's natural width.
The image size and EXIF data is loaded before the image such
that the window can be shown in the right dimension while
loading the actual image.
This commit is contained in:
Sophie Herold 2022-11-16 20:14:39 +01:00 • committed by Christopher Davis
parent 68221bacba
commit e04992b46d
12 changed files with 389 additions and 79 deletions

2
Cargo.lock generated
View file

@ -1044,6 +1044,8 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"ashpd", "ashpd",
"async-global-executor",
"async-std",
"futures", "futures",
"futures-channel", "futures-channel",
"gettext-rs", "gettext-rs",

View file

@ -2,9 +2,12 @@
name = "loupe" name = "loupe"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
rust-version = "1.65"
[dependencies] [dependencies]
anyhow = "1.0.66" anyhow = "1.0.66"
async-global-executor = "2.3.1"
async-std = "1.12.0"
gtk-macros = "0.3.0" gtk-macros = "0.3.0"
kamadak-exif = "0.5.5" kamadak-exif = "0.5.5"
log = "0.4.17" log = "0.4.17"

View file

@ -53,11 +53,13 @@
<object class="AdwStatusPage" id="error_page"> <object class="AdwStatusPage" id="error_page">
<property name="icon-name">image-missing-symbolic</property> <property name="icon-name">image-missing-symbolic</property>
<property name="title" translatable="yes">Could not Load Image</property> <property name="title" translatable="yes">Could not Load Image</property>
<property name="description" translatable="yes">This image could be corrupted or may use an unsupported file format.</property> <property name="description" bind-source="LpImagePage" bind-property="error"/>
</object> </object>
</child> </child>
<child> <child>
<object class="GtkScrolledWindow" id="scrolled_window"> <object class="GtkScrolledWindow" id="scrolled_window">
<property name="propagate-natural-height">True</property>
<property name="propagate-natural-width">True</property>
<property name="child"> <property name="child">
<object class="LpImage" id="image"> <object class="LpImage" id="image">
<child> <child>

View file

@ -5,7 +5,7 @@
<property name="focusable">True</property> <property name="focusable">True</property>
<property name="focus-on-click">True</property> <property name="focus-on-click">True</property>
<property name="child"> <property name="child">
<object class="GtkOverlay"> <object class="GtkOverlay" id="bin_child">
<property name="hexpand">True</property> <property name="hexpand">True</property>
<child> <child>
<object class="AdwCarousel" id="carousel"> <object class="AdwCarousel" id="carousel">

View file

@ -24,6 +24,7 @@ use adw::prelude::*;
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
use crate::config; use crate::config;
use crate::widgets::{LpImage, LpImagePage};
use crate::window::LpWindow; use crate::window::LpWindow;
mod imp { mod imp {
@ -74,7 +75,35 @@ mod imp {
for file in files { for file in files {
let win = LpWindow::new(&*application); let win = LpWindow::new(&*application);
win.set_image_from_file(file, true); win.set_image_from_file(file, true);
win.show();
// show window when image size is known
let watch = win
.imp()
.image_view
.property_expression("current-page-strict")
.chain_property::<LpImagePage>("image")
.chain_property::<LpImage>("image-size")
.watch(
glib::Object::NONE,
glib::clone!(@weak win => move || {
win.image_size_ready();
}),
);
win.imp().watch_image_size.replace(Some(watch));
// show if loading image errors
let watch = win
.imp()
.image_view
.property_expression("current-page-strict")
.chain_property::<LpImagePage>("error")
.watch(
glib::Object::NONE,
glib::clone!(@weak win => move || {
win.image_error();
}),
);
win.imp().watch_image_error.replace(Some(watch));
} }
} }
} }

View file

@ -81,19 +81,19 @@ impl LpFileModel {
vec.push(file); vec.push(file);
} }
model.imp().directory.set(directory.clone()).unwrap(); model.imp().directory.set(directory).unwrap();
Ok(model) Ok(model)
} }
pub fn load_directory(&self) -> anyhow::Result<()> { pub async fn load_directory(&self) -> anyhow::Result<()> {
let directory = self.imp().directory.get().unwrap(); let directory = self.imp().directory.get().unwrap().clone();
{
// Here we use a nested scope so that the mutable borrow only lasts as long as we need it
let mut vec = self.imp().inner.borrow_mut();
let original_vec: Vec<Option<std::path::PathBuf>> = let original_vec: Vec<Option<std::path::PathBuf>> =
vec.iter().map(|x| x.path()).collect(); self.imp().inner.borrow().iter().map(|x| x.path()).collect();
let new_files_result = util::spawn("list-files", move || {
let mut vec = Vec::new();
let enumerator = directory let enumerator = directory
.enumerate_children( .enumerate_children(
@ -105,7 +105,7 @@ impl LpFileModel {
gio::FileQueryInfoFlags::NONE, gio::FileQueryInfoFlags::NONE,
gio::Cancellable::NONE, gio::Cancellable::NONE,
) )
.context(i18n("Directory does not exist."))?; .context(i18n("Could not list other files in directory."))?;
// Filter out non-images; For now we support "all" image types. // Filter out non-images; For now we support "all" image types.
enumerator.for_each(|info| { enumerator.for_each(|info| {
@ -123,8 +123,23 @@ impl LpFileModel {
} }
}); });
//anyhow::Result::Ok(vec)
Ok::<_, anyhow::Error>(vec)
})
.await;
let Ok(new_files) = new_files_result else {
log::debug!("Thread listing directory canceled.");
return Ok(());
};
{
// Here we use a nested scope so that the mutable borrow only lasts as long as we need it
let mut files = self.imp().inner.borrow_mut();
files.append(&mut new_files?);
// Then sort by name. // Then sort by name.
vec.sort_by(util::compare_by_name); files.sort_by(util::compare_by_name);
} }
Ok(()) Ok(())

View file

@ -37,7 +37,7 @@ mod deps {
use application::LpApplication; use application::LpApplication;
fn main() { fn main() {
pretty_env_logger::init(); pretty_env_logger::init_timed();
setlocale(LocaleCategory::LcAll, ""); setlocale(LocaleCategory::LcAll, "");
bindtextdomain("loupe", config::LOCALEDIR).unwrap(); bindtextdomain("loupe", config::LOCALEDIR).unwrap();

View file

@ -5,7 +5,7 @@ use glib::translate::*;
use anyhow::Context; use anyhow::Context;
use std::fmt::Write; use std::fmt::{Debug, Write};
pub fn utf8_collate_key_for_filename(filename: &str) -> String { pub fn utf8_collate_key_for_filename(filename: &str) -> String {
unsafe { unsafe {
@ -62,3 +62,15 @@ pub fn compare_by_name(file_a: &gio::File, file_b: &gio::File) -> std::cmp::Orde
utf8_collate_key_for_filename(&name_a).cmp(&utf8_collate_key_for_filename(&name_b)) utf8_collate_key_for_filename(&name_a).cmp(&utf8_collate_key_for_filename(&name_b))
} }
pub async fn spawn<T: Debug + Send + 'static>(
name: &str,
f: impl Fn() -> T + Send + 'static,
) -> async_std::io::Result<T> {
log::trace!("Starting thread '{name}'");
Ok(async_std::task::Builder::new()
.name(name.to_string())
.spawn(async_global_executor::spawn_blocking(f))?
.await)
}

View file

@ -20,12 +20,14 @@ use crate::deps::*;
use adw::{prelude::*, subclass::prelude::*}; use adw::{prelude::*, subclass::prelude::*};
use anyhow::Context;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use once_cell::unsync::OnceCell; use once_cell::unsync::OnceCell;
use std::cell::{Cell, RefCell}; use std::cell::{Cell, RefCell};
use crate::i18n::i18n;
use crate::image_metadata::ImageMetadata; use crate::image_metadata::ImageMetadata;
use crate::util;
const ZOOM_ANIMATION_DURATION: u32 = 200; const ZOOM_ANIMATION_DURATION: u32 = 200;
const ROTATION_ANIMATION_DURATION: u32 = 200; const ROTATION_ANIMATION_DURATION: u32 = 200;
@ -43,6 +45,9 @@ mod imp {
pub struct LpImage { pub struct LpImage {
pub file: RefCell<Option<gio::File>>, pub file: RefCell<Option<gio::File>>,
pub texture: RefCell<Option<gdk::Texture>>, pub texture: RefCell<Option<gdk::Texture>>,
/// Set when image is completely loaded and ready for displaying
pub is_loaded: Cell<bool>,
/// Rotation final value (can differ from `rotation` during animation) /// Rotation final value (can differ from `rotation` during animation)
pub rotation_target: Cell<f64>, pub rotation_target: Cell<f64>,
/// Rotated presentation of original image in degrees clockwise /// Rotated presentation of original image in degrees clockwise
@ -75,6 +80,8 @@ mod imp {
/// Currently EXIF data /// Currently EXIF data
pub image_metadata: RefCell<ImageMetadata>, pub image_metadata: RefCell<ImageMetadata>,
/// This will often be available earlier than the complete image
pub image_original_size: Cell<(i32, i32)>,
/// Current pointer position /// Current pointer position
pub pointer_position: Cell<Option<(f64, f64)>>, pub pointer_position: Cell<Option<(f64, f64)>>,
@ -105,6 +112,9 @@ mod imp {
glib::ParamSpecObject::builder::<gio::File>("file") glib::ParamSpecObject::builder::<gio::File>("file")
.read_only() .read_only()
.build(), .build(),
glib::ParamSpecBoolean::builder("is-loaded")
.read_only()
.build(),
glib::ParamSpecDouble::builder("rotation") glib::ParamSpecDouble::builder("rotation")
.explicit_notify() .explicit_notify()
.build(), .build(),
@ -120,6 +130,9 @@ mod imp {
glib::ParamSpecBoolean::builder("is-max-zoom") glib::ParamSpecBoolean::builder("is-max-zoom")
.read_only() .read_only()
.build(), .build(),
glib::ParamSpecVariant::builder("image-size", glib::VariantTy::TUPLE)
.read_only()
.build(),
glib::ParamSpecOverride::for_interface::<gtk::Scrollable>("hadjustment"), glib::ParamSpecOverride::for_interface::<gtk::Scrollable>("hadjustment"),
glib::ParamSpecOverride::for_interface::<gtk::Scrollable>("vadjustment"), glib::ParamSpecOverride::for_interface::<gtk::Scrollable>("vadjustment"),
glib::ParamSpecOverride::for_interface::<gtk::Scrollable>("hscroll-policy"), glib::ParamSpecOverride::for_interface::<gtk::Scrollable>("hscroll-policy"),
@ -134,11 +147,13 @@ mod imp {
let obj = self.instance(); let obj = self.instance();
match pspec.name() { match pspec.name() {
"file" => obj.file().to_value(), "file" => obj.file().to_value(),
"is-loaded" => obj.is_loaded().to_value(),
"rotation" => obj.rotation().to_value(), "rotation" => obj.rotation().to_value(),
"mirrored" => obj.mirrored().to_value(), "mirrored" => obj.mirrored().to_value(),
"zoom" => obj.zoom().to_value(), "zoom" => obj.zoom().to_value(),
"best-fit" => obj.is_best_fit().to_value(), "best-fit" => obj.is_best_fit().to_value(),
"is-max-zoom" => obj.is_max_zoom().to_value(), "is-max-zoom" => obj.is_max_zoom().to_value(),
"image-size" => obj.image_size().to_variant().to_value(),
// don't use getter functions here sicne they can return a fake adjustment // don't use getter functions here sicne they can return a fake adjustment
"hadjustment" => self.hadjustment.borrow().to_value(), "hadjustment" => self.hadjustment.borrow().to_value(),
"vadjustment" => self.vadjustment.borrow().to_value(), "vadjustment" => self.vadjustment.borrow().to_value(),
@ -450,6 +465,81 @@ mod imp {
snapshot.restore(); snapshot.restore();
} }
} }
fn measure(&self, orientation: gtk::Orientation, _for_size: i32) -> (i32, i32, i32, i32) {
let (image_width, image_height) = self.instance().image_size();
if image_width > 0 && image_height > 0 {
if let Some(display) = gdk::Display::default() {
if let Some(native) = self.instance().native() {
let hidpi_scale = self.instance().scale_factor() as f64;
// get monitor information
let monitor = display.monitor_at_surface(&native.surface());
let monitor_geometry = monitor.geometry();
// TODO: Per documentation those dimensions should not be physical pixels.
// But on Wayland they are physiscal pixels and on X11 not.
// Taking the version that works on Wayland for now.
// <https://gitlab.gnome.org/GNOME/gtk/-/issues/5391>
let monitor_width = monitor_geometry.width() as f64 - 40.;
let monitor_height = monitor_geometry.height() as f64 - 60.;
// areas
let monitor_area = monitor_width * monitor_height;
let image_area = image_width as f64 * image_height as f64;
let ocuppy_area_factor = if monitor_area < 1024. * 768. {
// for small monitors occupy 80% of the area
0.8
} else {
// for large monitors occupy 30% of the area
0.3
};
// factor for width and height that will achive the desired area occupation
// derived from:
// monitor_area * ocuppy_area_factor ==
// (image_width * size_scale) * (image_height * size_scale)
let size_scale = f64::sqrt(monitor_area / image_area * ocuppy_area_factor);
// ensure that we never increase image size
let target_scale = f64::min(1.0, size_scale);
let mut nat_width = image_width as f64 * target_scale;
let mut nat_height = image_height as f64 * target_scale;
// scale down if targeted occupation does not fit in one direction
if nat_width > monitor_width {
nat_width = monitor_width;
nat_height = nat_height * monitor_width / nat_width;
}
// same for other direction
if nat_height > monitor_height {
nat_height = monitor_height;
nat_width = nat_width * monitor_height / nat_height;
}
let size = match orientation {
gtk::Orientation::Horizontal => (nat_width / hidpi_scale).round(),
gtk::Orientation::Vertical => (nat_height / hidpi_scale).round(),
_ => unreachable!(),
};
return (0, size as i32, -1, -1);
}
}
}
// fallback if monitor size or image size is not known:
// use original image size and hope for the best
// TODO: We could use a small default monitor size estimate instead
let size = match orientation {
gtk::Orientation::Horizontal => image_width,
gtk::Orientation::Vertical => image_height,
_ => unreachable!(),
};
(0, size, -1, -1)
}
} }
impl ScrollableImpl for LpImage {} impl ScrollableImpl for LpImage {}
@ -462,26 +552,73 @@ glib::wrapper! {
} }
impl LpImage { impl LpImage {
pub fn set_texture_with_file(&self, texture: gdk::Texture, source_file: &gio::File) { pub async fn load(&self, file: &gio::File) -> anyhow::Result<()> {
let imp = self.imp(); let imp = self.imp();
imp.texture.replace(Some(texture)); if let Some(path) = file.path() {
imp.file.replace(Some(source_file.clone())); imp.file.replace(Some(file.clone()));
let metadata = ImageMetadata::load(source_file); if !path.is_file() {
return Err(anyhow::Error::msg(i18n("File does not exist")));
}
log::debug!("Loading file {}", path.display());
log::debug!("Loading EXIF");
let metadata = util::spawn(
"image-load-exif",
glib::clone!(@strong file => move || ImageMetadata::load(&file)),
)
.await?;
let orientation = metadata.orientation(); let orientation = metadata.orientation();
imp.image_metadata.replace(metadata); imp.image_metadata.replace(metadata);
imp.rotation_target.set(-orientation.rotation); imp.rotation_target.set(-orientation.rotation);
imp.rotation.set(-orientation.rotation); imp.rotation.set(-orientation.rotation);
imp.mirrored.set(orientation.mirrored); imp.mirrored.set(orientation.mirrored);
log::debug!("Loading image size");
let (_, x, y) = util::spawn(
"image-load-size",
glib::clone!(@strong path => move || gdk_pixbuf::Pixbuf::file_info(&path)),
)
.await?
.context(i18n("Failed to load image size"))?;
imp.image_original_size.set((x, y));
self.notify("image-size");
log::debug!("Loading complete image");
let texture = util::spawn(
"image-load-image",
glib::clone!(@strong path => move || {
let pixbuf = gdk_pixbuf::Pixbuf::from_file(&path)?;
log::debug!("Creating texture");
Ok::<_,anyhow::Error>(gdk::Texture::for_pixbuf(&pixbuf))
}),
)
.await?
.context(i18n("Failed to load image size"))?;
imp.texture.replace(Some(texture));
self.configure_adjustments(); self.configure_adjustments();
self.queue_draw(); self.queue_draw();
self.queue_allocate(); self.queue_allocate();
self.queue_resize(); self.queue_resize();
log::debug!("Image loaded and scheduled for drawing");
imp.is_loaded.set(true);
self.notify("is-loaded");
} else {
log::error!("File has not path");
}
Ok(())
}
pub fn is_loaded(&self) -> bool {
self.imp().is_loaded.get()
} }
/// Zoom level that makes the image fit in widget /// Zoom level that makes the image fit in widget
@ -807,6 +944,17 @@ impl LpImage {
} }
} }
/// Image size of original image with EXIF rotation applied
pub fn image_size(&self) -> (i32, i32) {
let orientation = self.imp().image_metadata.borrow().orientation();
if orientation.rotation.abs() == 90. {
let (x, y) = self.imp().image_original_size.get();
(y, x)
} else {
self.imp().image_original_size.get()
}
}
/// Image width with current zoom factor and rotation /// Image width with current zoom factor and rotation
/// ///
/// During rotation it is an interpolated size that does not /// During rotation it is an interpolated size that does not
@ -838,7 +986,7 @@ impl LpImage {
if let Some(adj) = self.imp().hadjustment.borrow().as_ref() { if let Some(adj) = self.imp().hadjustment.borrow().as_ref() {
adj.clone() adj.clone()
} else { } else {
log::debug!("Hadjustment not set yet: Using fake object"); log::trace!("Hadjustment not set yet: Using fake object");
gtk::Adjustment::default() gtk::Adjustment::default()
} }
} }
@ -860,7 +1008,7 @@ impl LpImage {
if let Some(adj) = self.imp().vadjustment.borrow().as_ref() { if let Some(adj) = self.imp().vadjustment.borrow().as_ref() {
adj.clone() adj.clone()
} else { } else {
log::debug!("Vadjustment not set yet: Using fake object"); log::trace!("Vadjustment not set yet: Using fake object");
gtk::Adjustment::default() gtk::Adjustment::default()
} }
} }

View file

@ -27,6 +27,7 @@ use gtk::CompositeTemplate;
use gtk_macros::spawn; use gtk_macros::spawn;
use once_cell::sync::{Lazy, OnceCell}; use once_cell::sync::{Lazy, OnceCell};
use std::cell::RefCell;
use crate::widgets::LpImage; use crate::widgets::LpImage;
@ -54,6 +55,9 @@ mod imp {
pub(super) press_gesture: TemplateChild<gtk::GestureLongPress>, pub(super) press_gesture: TemplateChild<gtk::GestureLongPress>,
pub(super) file: OnceCell<gio::File>, pub(super) file: OnceCell<gio::File>,
/// set if an error has occured and is shown on error_page
pub(super) error: RefCell<Option<String>>,
} }
#[glib::object_subclass] #[glib::object_subclass]
@ -74,9 +78,12 @@ mod imp {
impl ObjectImpl for LpImagePage { impl ObjectImpl for LpImagePage {
fn properties() -> &'static [glib::ParamSpec] { fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| { static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecObject::builder::<LpImage>("image") vec![
glib::ParamSpecObject::builder::<LpImage>("image")
.read_only() .read_only()
.build()] .build(),
glib::ParamSpecString::builder("error").read_only().build(),
]
}); });
PROPERTIES.as_ref() PROPERTIES.as_ref()
@ -86,6 +93,7 @@ mod imp {
let obj = self.instance(); let obj = self.instance();
match pspec.name() { match pspec.name() {
"image" => obj.image().to_value(), "image" => obj.image().to_value(),
"error" => obj.error().to_value(),
name => unimplemented!("property {name}"), name => unimplemented!("property {name}"),
} }
} }
@ -130,14 +138,15 @@ impl LpImagePage {
spawn!(clone!(@weak obj, @weak file => async move { spawn!(clone!(@weak obj, @weak file => async move {
let imp = obj.imp(); let imp = obj.imp();
match load_texture_from_file(&file).await { match imp.image.load(&file).await {
Ok(texture) => { Ok(()) => {
imp.image.set_texture_with_file(texture, &file);
imp.stack.set_visible_child(&*imp.scrolled_window); imp.stack.set_visible_child(&*imp.scrolled_window);
imp.spinner.set_spinning(false); imp.spinner.set_spinning(false);
}, },
Err(e) => { Err(e) => {
imp.error.replace(Some(e.to_string()));
imp.stack.set_visible_child(&*imp.error_page); imp.stack.set_visible_child(&*imp.error_page);
obj.notify("error");
imp.spinner.set_spinning(false); imp.spinner.set_spinning(false);
log::error!("Could not load image: {e}"); log::error!("Could not load image: {e}");
} }
@ -159,6 +168,10 @@ impl LpImagePage {
self.imp().image.texture() self.imp().image.texture()
} }
pub fn error(&self) -> Option<String> {
self.imp().error.borrow().clone()
}
pub fn content_provider(&self) -> Option<gdk::ContentProvider> { pub fn content_provider(&self) -> Option<gdk::ContentProvider> {
self.imp().image.content_provider() self.imp().image.content_provider()
} }
@ -172,16 +185,3 @@ impl LpImagePage {
imp.popover.popup(); imp.popover.popup();
} }
} }
async fn load_texture_from_file(file: &gio::File) -> Result<gdk::Texture, glib::Error> {
let (sender, receiver) = futures_channel::oneshot::channel();
let _ = std::thread::Builder::new()
.name("Load Texture".to_string())
.spawn(clone!(@weak file => move || {
let result = gdk::Texture::from_file(&file);
sender.send(result).unwrap()
}));
receiver.await.unwrap()
}

View file

@ -20,9 +20,9 @@
use crate::deps::*; use crate::deps::*;
use crate::i18n::*; use crate::i18n::*;
use adw::prelude::*;
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
use glib::clone; use glib::clone;
use gtk::prelude::*;
use gtk::CompositeTemplate; use gtk::CompositeTemplate;
use anyhow::{bail, Context}; use anyhow::{bail, Context};
@ -49,6 +49,10 @@ mod imp {
#[derive(Debug, Default, CompositeTemplate)] #[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/org/gnome/Loupe/gtk/image_view.ui")] #[template(resource = "/org/gnome/Loupe/gtk/image_view.ui")]
pub struct LpImageView { pub struct LpImageView {
/// Direct child of this Adw::Bin
#[template_child]
pub bin_child: TemplateChild<gtk::Widget>,
#[template_child] #[template_child]
pub carousel: TemplateChild<adw::Carousel>, pub carousel: TemplateChild<adw::Carousel>,
@ -56,6 +60,8 @@ mod imp {
pub current_model_index: Cell<u32>, pub current_model_index: Cell<u32>,
pub scrolling: Cell<bool>, pub scrolling: Cell<bool>,
pub current_page_strict: RefCell<Option<LpImagePage>>,
} }
#[glib::object_subclass] #[glib::object_subclass]
@ -112,6 +118,9 @@ mod imp {
self.parent_constructed(); self.parent_constructed();
// Manually mange widget layout, see `WidgetImpl` for details
obj.set_layout_manager(gtk::LayoutManager::NONE);
let source = gtk::DragSource::new(); let source = gtk::DragSource::new();
source.set_exclusive(true); source.set_exclusive(true);
@ -148,7 +157,40 @@ mod imp {
} }
} }
impl WidgetImpl for LpImageView {} impl WidgetImpl for LpImageView {
/// The main target of this manual implemtation is to provide a good
/// natural width that allows to have a neat size for newly opened `LpWindow`s.
/// The actual calculation of the image natural width happens in
/// the measure function of `LpImage`.
///
/// This manual implementation is necessary since AdwCarousel gives the
/// largest natural width of all of it's children. But we actually want the
/// one of the opened (current) image.
fn measure(&self, orientation: gtk::Orientation, for_size: i32) -> (i32, i32, i32, i32) {
// Measure child of AdwBin
let child_measure @ (child_min, _, _, _) =
self.bin_child.measure(orientation, for_size);
if let Some(page) = self.instance().current_page_strict() {
// measure `LpImage`
let (_, image_natural, _, _) = page.image().measure(orientation, for_size);
// Ensure that minimum size is not smaller than the one of the child.
// Also ensure that the natural width is not smaller than the minimal size.
// Both things are required by GTK.
(child_min, i32::max(child_min, image_natural), -1, -1)
} else {
// If there is no image, just pass through the normal measurement
child_measure
}
}
/// this is necessary because we do layout manually
fn size_allocate(&self, width: i32, height: i32, baseline: i32) {
self.bin_child.allocate(width, height, baseline, None);
}
}
impl BinImpl for LpImageView {} impl BinImpl for LpImageView {}
} }
@ -169,7 +211,6 @@ impl LpImageView {
self.build_model_from_file(file)?; self.build_model_from_file(file)?;
self.notify("active-file"); self.notify("active-file");
self.notify("current-page-strict");
Ok(()) Ok(())
} }
@ -206,22 +247,35 @@ impl LpImageView {
} else { } else {
let model = LpFileModel::from_file(file)?; let model = LpFileModel::from_file(file)?;
*model_store = Some(model.clone()); *model_store = Some(model.clone());
log::debug!("new model created"); log::debug!("first model created");
model model
} }
}; };
carousel.append(&LpImagePage::from_file(file)); let page = LpImagePage::from_file(file);
imp.current_page_strict.replace(Some(page.clone()));
self.notify("current-page-strict");
carousel.append(&page);
self.update_action_state(&model, 0); self.update_action_state(&model, 0);
model.load_directory()?; spawn!(glib::clone!(@weak self as obj, @strong file => async move {
if let Err(err) = model.load_directory().await {
log::warn!("Failed to load directory: {:?}", err);
obj.activate_action("win.show-toast", Some(&(err.to_string(), 1).to_variant()))
.unwrap();
return;
}
let index = model let index = model
.index_of(file) .index_of(&file)
.context(i18n("File not found in model. (TODO: bad message)"))?; .context("File not found in model.")
.unwrap();
log::debug!("Currently at file {index} in the directory"); log::debug!("Currently at file {index} in the directory");
self.fill_carousel(&model, index); obj.fill_carousel(&model, index);
self.update_action_state(&model, index); obj.update_action_state(&model, index);
}));
Ok(()) Ok(())
} }
@ -323,8 +377,13 @@ impl LpImageView {
} }
#[template_callback] #[template_callback]
fn page_changed_cb(&self, _index: u32, carousel: &adw::Carousel) { fn page_changed_cb(&self, index: u32, carousel: &adw::Carousel) {
let imp = self.imp(); let imp = self.imp();
imp.current_page_strict
.replace(carousel.nth_page(index).downcast().ok());
self.notify("current-page-strict");
let b = imp.model.borrow(); let b = imp.model.borrow();
let model = b.as_ref().unwrap(); let model = b.as_ref().unwrap();
let current = self.active_file().unwrap(); let current = self.active_file().unwrap();
@ -535,22 +594,7 @@ impl LpImageView {
/// Returns `None` during animations instead of returning the closest image /// Returns `None` during animations instead of returning the closest image
pub fn current_page_strict(&self) -> Option<LpImagePage> { pub fn current_page_strict(&self) -> Option<LpImagePage> {
let carousel = &self.imp().carousel; self.imp().current_page_strict.borrow().clone()
if carousel.position().fract() != 0. {
// there is no current page during animations
None
} else {
let pos = carousel.position().round() as u32;
if pos >= carousel.n_pages() {
log::warn!("Hit AdwCarousel bug");
None
} else if carousel.n_pages() > 0 {
carousel.nth_page(pos).downcast().ok()
} else {
None
}
}
} }
pub fn uri(&self) -> Option<url::Url> { pub fn uri(&self) -> Option<url::Url> {

View file

@ -30,6 +30,8 @@ use ashpd::WindowIdentifier;
use gtk_macros::spawn; use gtk_macros::spawn;
use std::cell::RefCell;
use crate::config; use crate::config;
use crate::util; use crate::util;
use crate::widgets::{LpImage, LpImagePage, LpImageView, LpPropertiesView}; use crate::widgets::{LpImage, LpImagePage, LpImageView, LpPropertiesView};
@ -77,6 +79,9 @@ mod imp {
pub properties_view: TemplateChild<LpPropertiesView>, pub properties_view: TemplateChild<LpPropertiesView>,
#[template_child] #[template_child]
pub drop_target: TemplateChild<gtk::DropTarget>, pub drop_target: TemplateChild<gtk::DropTarget>,
pub watch_image_size: RefCell<Option<gtk::ExpressionWatch>>,
pub watch_image_error: RefCell<Option<gtk::ExpressionWatch>>,
} }
#[glib::object_subclass] #[glib::object_subclass]
@ -449,6 +454,56 @@ impl LpWindow {
self.action_set_enabled("win.toggle-properties", enabled); self.action_set_enabled("win.toggle-properties", enabled);
} }
pub fn image_size_ready(&self) {
// if visible for whatever reason, don't do any resize
if self.is_visible() {
self.disconnect_present_watches();
return;
}
let image = self
.imp()
.image_view
.current_page_strict()
.map(|page| page.image());
if let Some(image) = image {
if image.image_size() > (0, 0) {
log::debug!("Showing window because image size is ready");
// this let's the window determine the default size from LpImage's natural size
self.set_default_size(-1, -1);
self.disconnect_present_watches();
self.present();
}
}
}
pub fn image_error(&self) {
if self.is_visible() {
self.disconnect_present_watches();
return;
}
let current_page = self.imp().image_view.current_page_strict();
if let Some(page) = current_page {
if page.error().is_some() {
log::debug!("Showin window because loading image failed");
self.disconnect_present_watches();
self.present();
}
}
}
fn disconnect_present_watches(&self) {
if let Some(watch) = self.imp().watch_image_size.take() {
watch.unwatch();
}
if let Some(watch) = self.imp().watch_image_error.take() {
watch.unwatch();
}
}
// Adapted from https://gitlab.gnome.org/GNOME/eog/-/blob/master/src/eog-window.c:eog_window_obtain_desired_size // Adapted from https://gitlab.gnome.org/GNOME/eog/-/blob/master/src/eog-window.c:eog_window_obtain_desired_size
pub fn resize_from_dimensions(&self, img_width: i32, img_height: i32) { pub fn resize_from_dimensions(&self, img_width: i32, img_height: i32) {
let imp = self.imp(); let imp = self.imp();