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:
parent
68221bacba
commit
e04992b46d
12 changed files with 389 additions and 79 deletions
|
|
@ -20,12 +20,14 @@ use crate::deps::*;
|
|||
|
||||
use adw::{prelude::*, subclass::prelude::*};
|
||||
|
||||
use anyhow::Context;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use once_cell::unsync::OnceCell;
|
||||
use std::cell::{Cell, RefCell};
|
||||
|
||||
use crate::i18n::i18n;
|
||||
use crate::image_metadata::ImageMetadata;
|
||||
use crate::util;
|
||||
|
||||
const ZOOM_ANIMATION_DURATION: u32 = 200;
|
||||
const ROTATION_ANIMATION_DURATION: u32 = 200;
|
||||
|
|
@ -43,6 +45,9 @@ mod imp {
|
|||
pub struct LpImage {
|
||||
pub file: RefCell<Option<gio::File>>,
|
||||
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)
|
||||
pub rotation_target: Cell<f64>,
|
||||
/// Rotated presentation of original image in degrees clockwise
|
||||
|
|
@ -75,6 +80,8 @@ mod imp {
|
|||
|
||||
/// Currently EXIF data
|
||||
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
|
||||
pub pointer_position: Cell<Option<(f64, f64)>>,
|
||||
|
|
@ -105,6 +112,9 @@ mod imp {
|
|||
glib::ParamSpecObject::builder::<gio::File>("file")
|
||||
.read_only()
|
||||
.build(),
|
||||
glib::ParamSpecBoolean::builder("is-loaded")
|
||||
.read_only()
|
||||
.build(),
|
||||
glib::ParamSpecDouble::builder("rotation")
|
||||
.explicit_notify()
|
||||
.build(),
|
||||
|
|
@ -120,6 +130,9 @@ mod imp {
|
|||
glib::ParamSpecBoolean::builder("is-max-zoom")
|
||||
.read_only()
|
||||
.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>("vadjustment"),
|
||||
glib::ParamSpecOverride::for_interface::<gtk::Scrollable>("hscroll-policy"),
|
||||
|
|
@ -134,11 +147,13 @@ mod imp {
|
|||
let obj = self.instance();
|
||||
match pspec.name() {
|
||||
"file" => obj.file().to_value(),
|
||||
"is-loaded" => obj.is_loaded().to_value(),
|
||||
"rotation" => obj.rotation().to_value(),
|
||||
"mirrored" => obj.mirrored().to_value(),
|
||||
"zoom" => obj.zoom().to_value(),
|
||||
"best-fit" => obj.is_best_fit().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
|
||||
"hadjustment" => self.hadjustment.borrow().to_value(),
|
||||
"vadjustment" => self.vadjustment.borrow().to_value(),
|
||||
|
|
@ -450,6 +465,81 @@ mod imp {
|
|||
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 {}
|
||||
|
|
@ -462,26 +552,73 @@ glib::wrapper! {
|
|||
}
|
||||
|
||||
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();
|
||||
|
||||
imp.texture.replace(Some(texture));
|
||||
imp.file.replace(Some(source_file.clone()));
|
||||
if let Some(path) = file.path() {
|
||||
imp.file.replace(Some(file.clone()));
|
||||
|
||||
let metadata = ImageMetadata::load(source_file);
|
||||
let orientation = metadata.orientation();
|
||||
if !path.is_file() {
|
||||
return Err(anyhow::Error::msg(i18n("File does not exist")));
|
||||
}
|
||||
|
||||
imp.image_metadata.replace(metadata);
|
||||
log::debug!("Loading file {}", path.display());
|
||||
|
||||
imp.rotation_target.set(-orientation.rotation);
|
||||
imp.rotation.set(-orientation.rotation);
|
||||
imp.mirrored.set(orientation.mirrored);
|
||||
log::debug!("Loading EXIF");
|
||||
let metadata = util::spawn(
|
||||
"image-load-exif",
|
||||
glib::clone!(@strong file => move || ImageMetadata::load(&file)),
|
||||
)
|
||||
.await?;
|
||||
let orientation = metadata.orientation();
|
||||
|
||||
self.configure_adjustments();
|
||||
imp.image_metadata.replace(metadata);
|
||||
imp.rotation_target.set(-orientation.rotation);
|
||||
imp.rotation.set(-orientation.rotation);
|
||||
imp.mirrored.set(orientation.mirrored);
|
||||
|
||||
self.queue_draw();
|
||||
self.queue_allocate();
|
||||
self.queue_resize();
|
||||
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.queue_draw();
|
||||
self.queue_allocate();
|
||||
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
|
||||
|
|
@ -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
|
||||
///
|
||||
/// 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() {
|
||||
adj.clone()
|
||||
} else {
|
||||
log::debug!("Hadjustment not set yet: Using fake object");
|
||||
log::trace!("Hadjustment not set yet: Using fake object");
|
||||
gtk::Adjustment::default()
|
||||
}
|
||||
}
|
||||
|
|
@ -860,7 +1008,7 @@ impl LpImage {
|
|||
if let Some(adj) = self.imp().vadjustment.borrow().as_ref() {
|
||||
adj.clone()
|
||||
} else {
|
||||
log::debug!("Vadjustment not set yet: Using fake object");
|
||||
log::trace!("Vadjustment not set yet: Using fake object");
|
||||
gtk::Adjustment::default()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ use gtk::CompositeTemplate;
|
|||
use gtk_macros::spawn;
|
||||
|
||||
use once_cell::sync::{Lazy, OnceCell};
|
||||
use std::cell::RefCell;
|
||||
|
||||
use crate::widgets::LpImage;
|
||||
|
||||
|
|
@ -54,6 +55,9 @@ mod imp {
|
|||
pub(super) press_gesture: TemplateChild<gtk::GestureLongPress>,
|
||||
|
||||
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]
|
||||
|
|
@ -74,9 +78,12 @@ mod imp {
|
|||
impl ObjectImpl for LpImagePage {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||
vec![glib::ParamSpecObject::builder::<LpImage>("image")
|
||||
.read_only()
|
||||
.build()]
|
||||
vec![
|
||||
glib::ParamSpecObject::builder::<LpImage>("image")
|
||||
.read_only()
|
||||
.build(),
|
||||
glib::ParamSpecString::builder("error").read_only().build(),
|
||||
]
|
||||
});
|
||||
|
||||
PROPERTIES.as_ref()
|
||||
|
|
@ -86,6 +93,7 @@ mod imp {
|
|||
let obj = self.instance();
|
||||
match pspec.name() {
|
||||
"image" => obj.image().to_value(),
|
||||
"error" => obj.error().to_value(),
|
||||
name => unimplemented!("property {name}"),
|
||||
}
|
||||
}
|
||||
|
|
@ -130,14 +138,15 @@ impl LpImagePage {
|
|||
|
||||
spawn!(clone!(@weak obj, @weak file => async move {
|
||||
let imp = obj.imp();
|
||||
match load_texture_from_file(&file).await {
|
||||
Ok(texture) => {
|
||||
imp.image.set_texture_with_file(texture, &file);
|
||||
match imp.image.load(&file).await {
|
||||
Ok(()) => {
|
||||
imp.stack.set_visible_child(&*imp.scrolled_window);
|
||||
imp.spinner.set_spinning(false);
|
||||
},
|
||||
Err(e) => {
|
||||
imp.error.replace(Some(e.to_string()));
|
||||
imp.stack.set_visible_child(&*imp.error_page);
|
||||
obj.notify("error");
|
||||
imp.spinner.set_spinning(false);
|
||||
log::error!("Could not load image: {e}");
|
||||
}
|
||||
|
|
@ -159,6 +168,10 @@ impl LpImagePage {
|
|||
self.imp().image.texture()
|
||||
}
|
||||
|
||||
pub fn error(&self) -> Option<String> {
|
||||
self.imp().error.borrow().clone()
|
||||
}
|
||||
|
||||
pub fn content_provider(&self) -> Option<gdk::ContentProvider> {
|
||||
self.imp().image.content_provider()
|
||||
}
|
||||
|
|
@ -172,16 +185,3 @@ impl LpImagePage {
|
|||
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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@
|
|||
use crate::deps::*;
|
||||
use crate::i18n::*;
|
||||
|
||||
use adw::prelude::*;
|
||||
use adw::subclass::prelude::*;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk::CompositeTemplate;
|
||||
|
||||
use anyhow::{bail, Context};
|
||||
|
|
@ -49,6 +49,10 @@ mod imp {
|
|||
#[derive(Debug, Default, CompositeTemplate)]
|
||||
#[template(resource = "/org/gnome/Loupe/gtk/image_view.ui")]
|
||||
pub struct LpImageView {
|
||||
/// Direct child of this Adw::Bin
|
||||
#[template_child]
|
||||
pub bin_child: TemplateChild<gtk::Widget>,
|
||||
|
||||
#[template_child]
|
||||
pub carousel: TemplateChild<adw::Carousel>,
|
||||
|
||||
|
|
@ -56,6 +60,8 @@ mod imp {
|
|||
pub current_model_index: Cell<u32>,
|
||||
|
||||
pub scrolling: Cell<bool>,
|
||||
|
||||
pub current_page_strict: RefCell<Option<LpImagePage>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
|
|
@ -112,6 +118,9 @@ mod imp {
|
|||
|
||||
self.parent_constructed();
|
||||
|
||||
// Manually mange widget layout, see `WidgetImpl` for details
|
||||
obj.set_layout_manager(gtk::LayoutManager::NONE);
|
||||
|
||||
let source = gtk::DragSource::new();
|
||||
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 {}
|
||||
}
|
||||
|
||||
|
|
@ -169,7 +211,6 @@ impl LpImageView {
|
|||
|
||||
self.build_model_from_file(file)?;
|
||||
self.notify("active-file");
|
||||
self.notify("current-page-strict");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -206,22 +247,35 @@ impl LpImageView {
|
|||
} else {
|
||||
let model = LpFileModel::from_file(file)?;
|
||||
*model_store = Some(model.clone());
|
||||
log::debug!("new model created");
|
||||
log::debug!("first model created");
|
||||
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);
|
||||
|
||||
model.load_directory()?;
|
||||
let index = model
|
||||
.index_of(file)
|
||||
.context(i18n("File not found in model. (TODO: bad message)"))?;
|
||||
log::debug!("Currently at file {index} in the 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;
|
||||
}
|
||||
|
||||
self.fill_carousel(&model, index);
|
||||
self.update_action_state(&model, index);
|
||||
let index = model
|
||||
.index_of(&file)
|
||||
.context("File not found in model.")
|
||||
.unwrap();
|
||||
log::debug!("Currently at file {index} in the directory");
|
||||
|
||||
obj.fill_carousel(&model, index);
|
||||
obj.update_action_state(&model, index);
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -323,8 +377,13 @@ impl LpImageView {
|
|||
}
|
||||
|
||||
#[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();
|
||||
|
||||
imp.current_page_strict
|
||||
.replace(carousel.nth_page(index).downcast().ok());
|
||||
self.notify("current-page-strict");
|
||||
|
||||
let b = imp.model.borrow();
|
||||
let model = b.as_ref().unwrap();
|
||||
let current = self.active_file().unwrap();
|
||||
|
|
@ -535,22 +594,7 @@ impl LpImageView {
|
|||
|
||||
/// Returns `None` during animations instead of returning the closest image
|
||||
pub fn current_page_strict(&self) -> Option<LpImagePage> {
|
||||
let carousel = &self.imp().carousel;
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
self.imp().current_page_strict.borrow().clone()
|
||||
}
|
||||
|
||||
pub fn uri(&self) -> Option<url::Url> {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue