Repository: flxzt/rnote (language: rust)

Issue:
Document position (Fixed size)
Hello, this is not a bug, just a minor convenience improvement.

## How it is
When opening a new document with fixed size (my default mode), then the document is aligned to the edge of the window 
<img src="https://github.com/flxzt/rnote/assets/96027888/566bb817-6f62-4982-92da-f30f162cf7e4" width="500">

Which is an issue for when one wants to import a PDF (or open a PDF form File Manager) since it is always offset a little bit.
<img src="https://github.com/flxzt/rnote/assets/96027888/878b6ccf-0453-4b25-9bcd-ca2b4a3e74ea" width="500">

## How it would be nicer
Is it possible to open new fixed size documents, scrolled down as much as possible, since one has to scroll down nonetheless to write at the top of the page?
<img src="https://github.com/flxzt/rnote/assets/96027888/4b93320f-d21e-4fa3-92db-4bdfe217c0a8" width="500">

This would also improve the import of PDF files (or when opening them from File Manager) since the offset would be 0 and the edges align.
<img src="https://github.com/flxzt/rnote/assets/96027888/ed354a96-7a85-4e27-bf37-487d833708cb" width="500">

## Setup
System: Fedora 38 Workstation
DE: Gnome 44
RNote version: v0.7.1 (Flathub)

Settings: Fixed Size Document, Size A4
File: Any A4 PDF ([Test PDF](https://github.com/flxzt/rnote/files/11981352/This.is.a.A4.Test.Document.pdf))
Import Settings: 
<img src="https://github.com/flxzt/rnote/assets/96027888/1232965e-f304-4671-abb2-d17042b11aa7" width="250">



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 92f71e1be4..3465058dd5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -62,3 +62,4 @@ jobs:
             misc/file-tests/v0-5-5-test.rnote\
             misc/file-tests/v0-5-13-test.rnote\
             misc/file-tests/v0-6-0-test.rnote\
+            misc/file-tests/v0-9-0-test.rnote\
diff --git a/crates/rnote-engine/src/camera.rs b/crates/rnote-engine/src/camera.rs
index 6a80308580..dd925eaa2b 100644
--- a/crates/rnote-engine/src/camera.rs
+++ b/crates/rnote-engine/src/camera.rs
@@ -33,13 +33,13 @@ pub struct Camera {
     #[serde(rename = "zoom")]
     zoom: f64,
     /// The temporary zoom. Is used to overlay the "permanent" zoom.
-    #[serde(rename = "temporary_zoom")]
+    #[serde(skip)]
     temporary_zoom: f64,
 
     /// The scale factor of the surface, usually 1.0 or 2.0 for high-dpi screens.
     ///
     /// This value could become a non-integer value in the future, so it is stored as float.
-    #[serde(rename = "scale_factor")]
+    #[serde(skip)]
     scale_factor: f64,
 
     #[serde(skip)]
@@ -49,7 +49,7 @@ pub struct Camera {
 impl Default for Camera {
     fn default() -> Self {
         Self {
-            offset: na::vector![0.0, 0.0],
+            offset: na::vector![-Self::OVERSHOOT_HORIZONTAL, -Self::OVERSHOOT_VERTICAL],
             size: na::vector![800.0, 600.0],
             zoom: 1.0,
             temporary_zoom: 1.0,
@@ -70,6 +70,15 @@ impl Camera {
     pub const OVERSHOOT_HORIZONTAL: f64 = 96.0;
     pub const OVERSHOOT_VERTICAL: f64 = 96.0;
 
+    pub fn clone_config(&self) -> Self {
+        Self {
+            offset: self.offset,
+            size: self.size,
+            zoom: self.zoom,
+            ..Default::default()
+        }
+    }
+
     pub fn with_zoom(mut self, zoom: f64) -> Self {
         self.zoom = zoom.clamp(Self::ZOOM_MIN, Self::ZOOM_MAX);
         self
diff --git a/crates/rnote-engine/src/document/mod.rs b/crates/rnote-engine/src/document/mod.rs
index 5d857a3e81..62386df69d 100644
--- a/crates/rnote-engine/src/document/mod.rs
+++ b/crates/rnote-engine/src/document/mod.rs
@@ -80,7 +80,7 @@ impl std::string::ToString for Layout {
     }
 }
 
-#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
 #[serde(default, rename = "document")]
 pub struct Document {
     #[serde(rename = "x", with = "rnote_compose::serialize::f64_dp3")]
@@ -123,6 +123,10 @@ impl Document {
         a: 0.35,
     };
 
+    pub fn clone_config(&self) -> Self {
+        self.clone()
+    }
+
     pub(crate) fn bounds(&self) -> Aabb {
         Aabb::new(
             na::point![self.x, self.y],
@@ -192,7 +196,7 @@ impl Document {
         let mut widget_flags = WidgetFlags::default();
         match self.layout {
             Layout::FixedSize => {
-                // do not resize in fixed size mode, if wanted use resize_doc_to_fit_content() for it.
+                // do not resize in fixed size mode, if wanted use resize_to_fit_content() for it.
             }
             Layout::ContinuousVertical => {
                 self.resize_doc_continuous_vertical_layout(store);
diff --git a/crates/rnote-engine/src/engine/export.rs b/crates/rnote-engine/src/engine/export.rs
index 282c9be189..11beae7c4a 100644
--- a/crates/rnote-engine/src/engine/export.rs
+++ b/crates/rnote-engine/src/engine/export.rs
@@ -338,7 +338,7 @@ impl Engine {
     /// Extract the current engine configuration.
     pub fn extract_engine_config(&self) -> EngineConfig {
         EngineConfig {
-            document: self.document,
+            document: self.document.clone_config(),
             pens_config: self.pens_config.clone(),
             penholder: self.penholder.clone_config(),
             import_prefs: self.import_prefs,
@@ -544,7 +544,7 @@ impl Engine {
         let doc_export_prefs =
             doc_export_prefs_override.unwrap_or(self.export_prefs.doc_export_prefs);
         let pages_content = self.extract_pages_content(doc_export_prefs.page_order);
-        let document = self.document;
+        let document = self.document.clone();
 
         rayon::spawn(move || {
             let result = || -> anyhow::Result<Vec<u8>> {
diff --git a/crates/rnote-engine/src/engine/import.rs b/crates/rnote-engine/src/engine/import.rs
index 67dfa7e9f3..03d4395623 100644
--- a/crates/rnote-engine/src/engine/import.rs
+++ b/crates/rnote-engine/src/engine/import.rs
@@ -279,7 +279,7 @@ impl Engine {
     }
 
     /// Import the generated strokes into the store.
-    pub fn import_generated_strokes(
+    pub fn import_generated_content(
         &mut self,
         strokes: Vec<(Stroke, Option<StrokeLayer>)>,
     ) -> WidgetFlags {
diff --git a/crates/rnote-engine/src/engine/mod.rs b/crates/rnote-engine/src/engine/mod.rs
index b71509cda4..0fa4193392 100644
--- a/crates/rnote-engine/src/engine/mod.rs
+++ b/crates/rnote-engine/src/engine/mod.rs
@@ -301,7 +301,8 @@ impl Engine {
         }
 
         EngineSnapshot {
-            document: self.document,
+            document: self.document.clone_config(),
+            camera: self.camera.clone_config(),
             stroke_components: Arc::clone(&store_history_entry.stroke_components),
             chrono_components: Arc::clone(&store_history_entry.chrono_components),
             chrono_counter: store_history_entry.chrono_counter,
@@ -310,13 +311,15 @@ impl Engine {
 
     /// Imports an engine snapshot. A save file should always be loaded with this method.
     pub fn load_snapshot(&mut self, snapshot: EngineSnapshot) -> WidgetFlags {
-        self.document = snapshot.document;
-        self.store.import_from_snapshot(&snapshot)
-            | self.current_pen_update_state()
-            | self.return_to_origin(None)
+        self.document = snapshot.document.clone_config();
+        self.camera = snapshot.camera.clone_config();
+        let mut widget_flags = self.store.import_from_snapshot(&snapshot)
             | self.doc_resize_autoexpand()
+            | self.current_pen_update_state()
             | self.background_regenerate_pattern()
-            | self.update_content_rendering_current_viewport()
+            | self.update_content_rendering_current_viewport();
+        widget_flags.update_view = true;
+        widget_flags
     }
 
     /// Records the current store state and saves it as a history entry.
@@ -560,6 +563,7 @@ impl Engine {
             widget_flags |= self.reinstall_pen_current_style()
                 | self.background_regenerate_pattern()
                 | self.update_content_rendering_current_viewport();
+            widget_flags.update_view = true;
         } else {
             widget_flags |= self.clear_rendering() | self.penholder.deinit_current_pen();
         }
@@ -668,7 +672,7 @@ impl Engine {
 
     /// Expand the doc to the camera when in autoexpanding layouts. called e.g. when dragging with touch.
     ///
-    /// Background rendering then needs to be updated.
+    /// Background and content rendering then needs to be updated.
     pub fn doc_expand_autoexpand(&mut self) -> WidgetFlags {
         self.document.expand_autoexpand(&self.camera)
     }
@@ -705,7 +709,7 @@ impl Engine {
 
     /// Update the viewport offset of the camera, clamped to mins and maxs values depending on the document layout.
     ///
-    /// Background and strokes rendering then need to be updated.
+    /// Background and content rendering then need to be updated.
     pub fn camera_set_offset(&mut self, offset: na::Vector2<f64>) -> WidgetFlags {
         self.camera.set_offset(offset, &self.document)
     }
@@ -714,21 +718,21 @@ impl Engine {
     ///
     /// Expands the document when in autoexpanding layouts.
     ///
-    /// Background and strokes rendering then need to be updated.
+    /// Background and content rendering then need to be updated.
     pub fn camera_set_offset_expand(&mut self, offset: na::Vector2<f64>) -> WidgetFlags {
         self.camera.set_offset(offset, &self.document) | self.doc_expand_autoexpand()
     }
 
     /// Update the viewport size of the camera.
     ///
-    /// Background and strokes rendering then need to be updated.
+    /// Background and content rendering then need to be updated.
     pub fn camera_set_size(&mut self, size: na::Vector2<f64>) -> WidgetFlags {
         self.camera.set_size(size)
     }
 
     /// Update the viewport size of the camera.
     ///
-    /// Background and strokes rendering then need to be updated.
+    /// Background and content rendering then need to be updated.
     pub fn camera_offset_mins_maxs(&self) -> (na::Vector2<f64>, na::Vector2<f64>) {
         self.camera.offset_lower_upper(&self.document)
     }
diff --git a/crates/rnote-engine/src/engine/rendering.rs b/crates/rnote-engine/src/engine/rendering.rs
index 973138cf36..797a5d4ae3 100644
--- a/crates/rnote-engine/src/engine/rendering.rs
+++ b/crates/rnote-engine/src/engine/rendering.rs
@@ -68,9 +68,8 @@ impl Engine {
     ///
     /// If the background pattern or zoom has changed, the background pattern needs to be regenerated first.
     pub fn update_rendering_current_viewport(&mut self) -> WidgetFlags {
-        let mut widget_flags = self.update_background_rendering_current_viewport();
-        widget_flags |= self.update_content_rendering_current_viewport();
-        widget_flags
+        self.update_background_rendering_current_viewport()
+            | self.update_content_rendering_current_viewport()
     }
 
     /// Clear the rendering of the entire engine (e.g. when it becomes off-screen).
diff --git a/crates/rnote-engine/src/engine/snapshot.rs b/crates/rnote-engine/src/engine/snapshot.rs
index 71268d55ea..109757bd41 100644
--- a/crates/rnote-engine/src/engine/snapshot.rs
+++ b/crates/rnote-engine/src/engine/snapshot.rs
@@ -4,7 +4,7 @@ use crate::engine::import::XoppImportPrefs;
 use crate::fileformats::{rnoteformat, xoppformat, FileFormatLoader};
 use crate::store::{ChronoComponent, StrokeKey};
 use crate::strokes::Stroke;
-use crate::{Document, Engine};
+use crate::{Camera, Document, Engine};
 use anyhow::Context;
 use futures::channel::oneshot;
 use serde::{Deserialize, Serialize};
@@ -17,6 +17,8 @@ use std::sync::Arc;
 pub struct EngineSnapshot {
     #[serde(rename = "document")]
     pub document: Document,
+    #[serde(rename = "camera")]
+    pub camera: Camera,
     #[serde(rename = "stroke_components")]
     pub stroke_components: Arc<HopSlotMap<StrokeKey, Arc<Stroke>>>,
     #[serde(rename = "chrono_components")]
@@ -29,6 +31,7 @@ impl Default for EngineSnapshot {
     fn default() -> Self {
         Self {
             document: Document::default(),
+            camera: Camera::default(),
             stroke_components: Arc::new(HopSlotMap::with_key()),
             chrono_components: Arc::new(SecondaryMap::new()),
             chrono_counter: 0,
@@ -39,7 +42,7 @@ impl Default for EngineSnapshot {
 impl EngineSnapshot {
     /// Loads a snapshot from the bytes of a .rnote file.
     ///
-    /// To import this snapshot into the current engine, use `import_snapshot()`.
+    /// To import this snapshot into the current engine, use [`Engine::load_snapshot()`].
     pub async fn load_from_rnote_bytes(bytes: Vec<u8>) -> anyhow::Result<Self> {
         let (snapshot_sender, snapshot_receiver) = oneshot::channel::<anyhow::Result<Self>>();
 
@@ -59,7 +62,7 @@ impl EngineSnapshot {
     }
     /// Loads from the bytes of a Xournal++ .xopp file.
     ///
-    /// To import this snapshot into the current engine, use `import_snapshot()`.
+    /// To import this snapshot into the current engine, use [`Engine::load_snapshot()`].
     pub async fn load_from_xopp_bytes(
         bytes: Vec<u8>,
         xopp_import_prefs: XoppImportPrefs,
diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/maj0min9.rs b/crates/rnote-engine/src/fileformats/rnoteformat/maj0min9.rs
new file mode 100644
index 0000000000..4b0ad9e3a9
--- /dev/null
+++ b/crates/rnote-engine/src/fileformats/rnoteformat/maj0min9.rs
@@ -0,0 +1,28 @@
+// Imports
+use super::maj0min6::RnoteFileMaj0Min6;
+use crate::Camera;
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct RnoteFileMaj0Min9 {
+    /// A snapshot of the engine.
+    #[serde(rename = "engine_snapshot")]
+    pub engine_snapshot: ijson::IValue,
+}
+
+impl TryFrom<RnoteFileMaj0Min6> for RnoteFileMaj0Min9 {
+    type Error = anyhow::Error;
+
+    fn try_from(mut value: RnoteFileMaj0Min6) -> Result<Self, Self::Error> {
+        let engine_snapsht = value
+            .engine_snapshot
+            .as_object_mut()
+            .ok_or_else(|| anyhow::anyhow!("engine snapshot is not a JSON object."))?;
+
+        engine_snapsht.insert("camera", ijson::to_value(&Camera::default())?);
+
+        Ok(Self {
+            engine_snapshot: value.engine_snapshot,
+        })
+    }
+}
diff --git a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs
index 52757e5e0a..7b1461c132 100644
--- a/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs
+++ b/crates/rnote-engine/src/fileformats/rnoteformat/mod.rs
@@ -9,11 +9,13 @@
 pub(crate) mod maj0min5patch8;
 pub(crate) mod maj0min5patch9;
 pub(crate) mod maj0min6;
+pub(crate) mod maj0min9;
 
 // Imports
 use self::maj0min5patch8::RnoteFileMaj0Min5Patch8;
 use self::maj0min5patch9::RnoteFileMaj0Min5Patch9;
 use self::maj0min6::RnoteFileMaj0Min6;
+use self::maj0min9::RnoteFileMaj0Min9;
 use super::{FileFormatLoader, FileFormatSaver};
 use anyhow::Context;
 use serde::{Deserialize, Serialize};
@@ -51,10 +53,10 @@ struct RnotefileWrapper {
 /// The Rnote file in the newest format version.
 ///
 /// This struct exists to allow for upgrading older versions before loading the file in.
-pub type RnoteFile = RnoteFileMaj0Min6;
+pub type RnoteFile = RnoteFileMaj0Min9;
 
 impl RnoteFile {
-    pub const SEMVER: &str = "0.8.2";
+    pub const SEMVER: &str = "0.9.0";
 }
 
 impl FileFormatLoader for RnoteFile {
@@ -65,31 +67,39 @@ impl FileFormatLoader for RnoteFile {
         .context("deserializing RnotefileWrapper from bytes failed.")?;
 
         // Conversions for older file format versions happen here
-        if semver::VersionReq::parse(">=0.5.10")
+        if semver::VersionReq::parse(">=0.9.0")
+            .unwrap()
+            .matches(&wrapper.version)
+        {
+            ijson::from_value::<RnoteFileMaj0Min9>(&wrapper.data)
+                .context("deserializing RnoteFileMaj0Min9 failed.")
+        } else if semver::VersionReq::parse(">=0.5.10")
             .unwrap()
             .matches(&wrapper.version)
         {
             ijson::from_value::<RnoteFileMaj0Min6>(&wrapper.data)
                 .context("deserializing RnoteFileMaj0Min6 failed.")
+                .and_then(RnoteFileMaj0Min9::try_from)
+                .context("converting RnoteFileMaj0Min6 to newest file version failed.")
         } else if semver::VersionReq::parse(">=0.5.9")
             .unwrap()
             .matches(&wrapper.version)
         {
-            RnoteFileMaj0Min6::try_from(
-                ijson::from_value::<RnoteFileMaj0Min5Patch9>(&wrapper.data)
-                    .context("deserializing RnoteFileMaj0Min5Patch9 failed.")?,
-            )
-            .context("converting RnoteFileMaj0Min5Patch9 to newest file version failed.")
+            ijson::from_value::<RnoteFileMaj0Min5Patch9>(&wrapper.data)
+                .context("deserializing RnoteFileMaj0Min5Patch9 failed.")
+                .and_then(RnoteFileMaj0Min6::try_from)
+                .and_then(RnoteFileMaj0Min9::try_from)
+                .context("converting RnoteFileMaj0Min5Patch9 to newest file version failed.")
         } else if semver::VersionReq::parse(">=0.5.0")
             .unwrap()
             .matches(&wrapper.version)
         {
-            RnoteFileMaj0Min5Patch9::try_from(
-                ijson::from_value::<RnoteFileMaj0Min5Patch8>(&wrapper.data)
-                    .context("deserializing RnoteFileMaj0Min5Patch8 failed")?,
-            )
-            .and_then(RnoteFileMaj0Min6::try_from)
-            .context("converting RnoteFileMaj0Min5Patch8 to newest file version failed.")
+            ijson::from_value::<RnoteFileMaj0Min5Patch8>(&wrapper.data)
+                .context("deserializing RnoteFileMaj0Min5Patch8 failed")
+                .and_then(RnoteFileMaj0Min5Patch9::try_from)
+                .and_then(RnoteFileMaj0Min6::try_from)
+                .and_then(RnoteFileMaj0Min9::try_from)
+                .context("converting RnoteFileMaj0Min5Patch8 to newest file version failed.")
         } else {
             Err(anyhow::anyhow!(
                 "failed to load rnote file from bytes, unsupported version: {}.",
diff --git a/crates/rnote-engine/src/meson.build b/crates/rnote-engine/src/meson.build
index 61d323a12d..cef1ed62d7 100644
--- a/crates/rnote-engine/src/meson.build
+++ b/crates/rnote-engine/src/meson.build
@@ -13,6 +13,7 @@ rnote_engine_sources = files(
     'fileformats/rnoteformat/maj0min5patch8.rs',
     'fileformats/rnoteformat/maj0min5patch9.rs',
     'fileformats/rnoteformat/maj0min6.rs',
+    'fileformats/rnoteformat/maj0min9.rs',
     'fileformats/rnoteformat/mod.rs',
     'fileformats/mod.rs',
     'fileformats/xoppformat.rs',
diff --git a/crates/rnote-ui/src/appwindow/mod.rs b/crates/rnote-ui/src/appwindow/mod.rs
index 34b91e1f6e..d178ea713e 100644
--- a/crates/rnote-ui/src/appwindow/mod.rs
+++ b/crates/rnote-ui/src/appwindow/mod.rs
@@ -217,10 +217,12 @@ impl RnAppWindow {
             canvas.set_empty(false);
         }
         if widget_flags.update_view {
-            let camera_offset = canvas.engine_ref().camera.offset();
-            // Keep the adjustment values in sync
-            canvas.hadjustment().unwrap().set_value(camera_offset[0]);
-            canvas.vadjustment().unwrap().set_value(camera_offset[1]);
+            let widget_size = canvas.widget_size();
+            let offset_mins_maxs = canvas.engine_ref().camera_offset_mins_maxs();
+            let offset = canvas.engine_ref().camera.offset();
+            // Keep the adjustments configuration in sync
+            canvas.configure_adjustments(widget_size, offset_mins_maxs, offset);
+            canvas.queue_resize();
         }
         if widget_flags.zoomed_temporarily {
             let total_zoom = canvas.engine_ref().camera.total_zoom();
@@ -473,13 +475,14 @@ impl RnAppWindow {
                             appwindow.active_tab_wrapper()
                         };
                         let (bytes, _) = input_file.load_bytes_future().await?;
-                        wrapper
+                        let widget_flags = wrapper
                             .canvas()
                             .load_in_rnote_bytes(bytes.to_vec(), input_file.path())
                             .await?;
                         if rnote_file_new_tab {
                             appwindow.append_wrapper_new_tab(&wrapper);
                         }
+                        appwindow.handle_widget_flags(widget_flags, &wrapper.canvas());
                         true
                     }
                 }
diff --git a/crates/rnote-ui/src/canvas/canvaslayout.rs b/crates/rnote-ui/src/canvas/canvaslayout.rs
index 442cfdae7a..e1b927d789 100644
--- a/crates/rnote-ui/src/canvas/canvaslayout.rs
+++ b/crates/rnote-ui/src/canvas/canvaslayout.rs
@@ -46,7 +46,7 @@ mod imp {
         ) -> (i32, i32, i32, i32) {
             let canvas = widget.downcast_ref::<RnCanvas>().unwrap();
             let total_zoom = canvas.engine_ref().camera.total_zoom();
-            let document = canvas.engine_ref().document;
+            let document = canvas.engine_ref().document.clone();
 
             if orientation == Orientation::Horizontal {
                 let natural_width = (document.width * total_zoom
@@ -66,39 +66,20 @@ mod imp {
             let canvas = widget.downcast_ref::<RnCanvas>().unwrap();
             let hadj = canvas.hadjustment().unwrap();
             let vadj = canvas.vadjustment().unwrap();
-
-            let (offset_mins, offset_maxs) = canvas.engine_ref().camera_offset_mins_maxs();
+            let hadj_value = hadj.value();
+            let vadj_value = vadj.value();
             let new_size = na::vector![width as f64, height as f64];
+            let offset_mins_maxs = canvas.engine_ref().camera_offset_mins_maxs();
+            let new_offset = na::vector![hadj_value, vadj_value];
+            let old_viewport = self.old_viewport.get();
+            let new_viewport = canvas.engine_ref().camera.viewport();
 
-            hadj.configure(
-                // This gets clamped to the lower and upper values
-                hadj.value(),
-                offset_mins[0],
-                offset_maxs[0],
-                0.1 * new_size[0],
-                0.9 * new_size[0],
-                new_size[0],
-            );
-
-            vadj.configure(
-                // This gets clamped to the lower and upper values
-                vadj.value(),
-                offset_mins[1],
-                offset_maxs[1],
-                0.1 * new_size[1],
-                0.9 * new_size[1],
-                new_size[1],
-            );
-
-            let new_offset = na::vector![hadj.value(), vadj.value()];
+            canvas.configure_adjustments(new_size, offset_mins_maxs, new_offset);
 
             // Update the camera
             let _ = canvas.engine_mut().camera_set_offset(new_offset);
             let _ = canvas.engine_mut().camera_set_size(new_size);
 
-            let new_viewport = canvas.engine_ref().camera.viewport();
-            let old_viewport = self.old_viewport.get();
-
             // We only extend the viewport by a (tweakable) fraction of the margin, because we want to trigger rendering
             // before we reach it. This has two advantages: Strokes that might take longer to render have a head start
             // while still being out of view, and the rendering gets triggered more often, so not that many strokes
diff --git a/crates/rnote-ui/src/canvas/imexport.rs b/crates/rnote-ui/src/canvas/imexport.rs
index 049b4ba3ef..b289b951c9 100644
--- a/crates/rnote-ui/src/canvas/imexport.rs
+++ b/crates/rnote-ui/src/canvas/imexport.rs
@@ -6,39 +6,51 @@ use rnote_compose::ext::Vector2Ext;
 use rnote_engine::engine::export::{DocExportPrefs, DocPagesExportPrefs, SelectionExportPrefs};
 use rnote_engine::engine::{EngineSnapshot, StrokeContent};
 use rnote_engine::strokes::Stroke;
+use rnote_engine::WidgetFlags;
 use std::ops::Range;
 use std::path::Path;
 
 impl RnCanvas {
+    /// Load the bytes of a `.rnote` file and imports it into the engine.
+    ///
+    /// `file_path` is optional but needs to be supplied when the origin file should be tracked.
+    ///
+    /// The function returns `WidgetFlags` instead of emitting the `handle_signal_flags` signal, because a signal
+    /// handler might not yet be connected when this function is called.
     pub(crate) async fn load_in_rnote_bytes<P>(
         &self,
         bytes: Vec<u8>,
         file_path: Option<P>,
-    ) -> anyhow::Result<()>
+    ) -> anyhow::Result<WidgetFlags>
     where
         P: AsRef<Path>,
     {
         let engine_snapshot = EngineSnapshot::load_from_rnote_bytes(bytes).await?;
         let widget_flags = self.engine_mut().load_snapshot(engine_snapshot);
-        self.emit_handle_widget_flags(widget_flags);
 
-        if let Some(file_path) = file_path {
-            let file = gio::File::for_path(file_path);
+        if file_path.is_some() {
             self.dismiss_output_file_modified_toast();
-            self.set_output_file(Some(file));
         }
+        self.set_output_file(file_path.map(gio::File::for_path));
         self.set_unsaved_changes(false);
         self.set_empty(false);
-        Ok(())
+        Ok(widget_flags)
     }
 
+    /// Reload the engine from the file that is set as origin file.
+    ///
+    /// If the origin file is set to None, this does nothing and returns an error.
     pub(crate) async fn reload_from_disk(&self) -> anyhow::Result<()> {
         let Some(output_file) = self.output_file() else {
-            return Ok(());
+            return Err(anyhow::anyhow!(
+                "Failed to reload file from disk, no file path saved."
+            ));
         };
         let (bytes, _) = output_file.load_bytes_future().await?;
-        self.load_in_rnote_bytes(bytes.to_vec(), output_file.path())
+        let widget_flags = self
+            .load_in_rnote_bytes(bytes.to_vec(), output_file.path())
             .await?;
+        self.emit_handle_widget_flags(widget_flags);
         Ok(())
     }
 
@@ -55,10 +67,12 @@ impl RnCanvas {
         Ok(())
     }
 
+    /// Loads in bytes from a vector image and imports it.
+    ///
+    /// `target_pos` is in coordinate space of the doc.
     pub(crate) async fn load_in_vectorimage_bytes(
         &self,
         bytes: Vec<u8>,
-        // In coordinate space of the doc
         target_pos: Option<na::Vector2<f64>>,
     ) -> anyhow::Result<()> {
         let pos = target_pos.unwrap_or_else(|| {
@@ -74,26 +88,27 @@ impl RnCanvas {
                 ])
         });
 
-        // we need the split the import operation between generate_vectorimage_from_bytes() which returns a receiver and import_generated_strokes(),
-        // to avoid borrowing the entire engine refcell while awaiting the stroke
+        // we need the split the import operation between generate_vectorimage_from_bytes()
+        // which returns a receiver and import_generated_content(),
+        // to avoid borrowing the entire engine refcell while awaiting the generated stroke
         let vectorimage_receiver = self
             .engine_mut()
             .generate_vectorimage_from_bytes(pos, bytes);
         let vectorimage = vectorimage_receiver.await??;
-
         let widget_flags = self
             .engine_mut()
-            .import_generated_strokes(vec![(Stroke::VectorImage(vectorimage), None)]);
+            .import_generated_content(vec![(Stroke::VectorImage(vectorimage), None)]);
 
         self.emit_handle_widget_flags(widget_flags);
         Ok(())
     }
 
-    /// Target position is in the coordinate space of the doc
+    /// Loads in bytes from a bitmap image and imports it.
+    ///
+    /// `target_pos` is in coordinate space of the doc.
     pub(crate) async fn load_in_bitmapimage_bytes(
         &self,
         bytes: Vec<u8>,
-        // In the coordinate space of the doc
         target_pos: Option<na::Vector2<f64>>,
     ) -> anyhow::Result<()> {
         let pos = target_pos.unwrap_or_else(|| {
@@ -116,13 +131,15 @@ impl RnCanvas {
 
         let widget_flags = self
             .engine_mut()
-            .import_generated_strokes(vec![(Stroke::BitmapImage(bitmapimage), None)]);
+            .import_generated_content(vec![(Stroke::BitmapImage(bitmapimage), None)]);
 
         self.emit_handle_widget_flags(widget_flags);
         Ok(())
     }
 
-    /// Target position is in the coordinate space of the doc
+    /// Loads in bytes from a pdf and imports it.
+    ///
+    /// `target_pos` is in coordinate space of the doc.
     pub(crate) async fn load_in_pdf_bytes(
         &self,
         bytes: Vec<u8>,
@@ -147,13 +164,15 @@ impl RnCanvas {
             .generate_pdf_pages_from_bytes(bytes, pos, page_range);
         let strokes = strokes_receiver.await??;
 
-        let widget_flags = self.engine_mut().import_generated_strokes(strokes);
+        let widget_flags = self.engine_mut().import_generated_content(strokes);
 
         self.emit_handle_widget_flags(widget_flags);
         Ok(())
     }
 
-    /// Target position is in the coordinate space of the doc
+    /// Imports a text.
+    ///
+    /// `target_pos` is in coordinate space of the doc.
     pub(crate) fn load_in_text(
         &self,
         text: String,
@@ -176,7 +195,9 @@ impl RnCanvas {
         Ok(())
     }
 
-    /// Deserializes the stroke content and inserts it into the engine. The data is usually coming from the clipboard, drop source, etc.
+    /// Deserializes the stroke content and inserts it into the engine.
+    ///
+    /// The data is usually coming from the clipboard, drop source, etc.
     pub(crate) async fn insert_stroke_content(&self, json_string: String) -> anyhow::Result<()> {
         let (oneshot_sender, oneshot_receiver) =
             oneshot::channel::<anyhow::Result<StrokeContent>>();
@@ -210,8 +231,8 @@ impl RnCanvas {
 
     /// Saves the document to the given file.
     ///
-    /// Returns Ok(true) if saved successfully, Ok(false) when a save is already in progress and no file operatiosn were executed,
-    /// Err(e) when saving failed in any way.
+    /// Returns Ok(true) if saved successfully, Ok(false) when a save is already in progress and no file operatiosn were
+    /// executed, Err(e) when saving failed in any way.
     pub(crate) async fn save_document_to_file(&self, file: &gio::File) -> anyhow::Result<bool> {
         // skip saving when it is already in progress
         if self.save_in_progress() {
@@ -287,8 +308,8 @@ impl RnCanvas {
     }
 
     /// Exports document pages
-    /// file_stem_name: the stem name of the created files. This is extended by an enumeration of the page number and file extension
-    /// overwrites existing files with the same name!
+    /// `file_stem_name`: the stem name of the created files. This is extended by an enumeration of the page number and
+    /// file extension overwrites existing files with the same name!
     pub(crate) async fn export_doc_pages(
         &self,
         dir: &gio::File,
diff --git a/crates/rnote-ui/src/canvas/mod.rs b/crates/rnote-ui/src/canvas/mod.rs
index fbdb1246d2..35cc2de0ad 100644
--- a/crates/rnote-ui/src/canvas/mod.rs
+++ b/crates/rnote-ui/src/canvas/mod.rs
@@ -21,6 +21,7 @@ use p2d::bounding_volume::Aabb;
 use rnote_compose::ext::AabbExt;
 use rnote_compose::penevent::PenState;
 use rnote_engine::ext::GrapheneRectExt;
+use rnote_engine::Camera;
 use rnote_engine::{Engine, WidgetFlags};
 use std::cell::{Cell, Ref, RefCell, RefMut};
 
@@ -476,6 +477,21 @@ mod imp {
         fn set_hadjustment_prop(&self, hadj: Option<Adjustment>) {
             let obj = self.obj();
 
+            let hadj_value = self
+                .hadjustment
+                .borrow()
+                .as_ref()
+                .map(|adj| adj.value())
+                .unwrap_or(-Camera::OVERSHOOT_HORIZONTAL);
+            let vadj_value = self
+                .vadjustment
+                .borrow()
+                .as_ref()
+                .map(|adj| adj.value())
+                .unwrap_or(-Camera::OVERSHOOT_VERTICAL);
+            let widget_size = obj.widget_size();
+            let offset_mins_maxs = obj.engine_ref().camera_offset_mins_maxs();
+
             if let Some(signal_id) = self.connections.borrow_mut().hadjustment.take() {
                 let old_adj = self.hadjustment.borrow().as_ref().unwrap().clone();
                 old_adj.disconnect(signal_id);
@@ -484,18 +500,40 @@ mod imp {
             if let Some(ref hadj) = hadj {
                 let signal_id =
                     hadj.connect_value_changed(clone!(@weak obj as canvas => move |_| {
-                        // this triggers a canvaslayout allocate() call, where the strokes rendering is updated based on some conditions
+                        // this triggers a canvaslayout allocate() call,
+                        // where the camera and content rendering is updated based on some conditions
                         canvas.queue_resize();
                     }));
 
                 self.connections.borrow_mut().hadjustment.replace(signal_id);
             }
             self.hadjustment.replace(hadj);
+
+            obj.configure_adjustments(
+                widget_size,
+                offset_mins_maxs,
+                na::vector![hadj_value, vadj_value],
+            );
         }
 
         fn set_vadjustment_prop(&self, vadj: Option<Adjustment>) {
             let obj = self.obj();
 
+            let hadj_value = self
+                .hadjustment
+                .borrow()
+                .as_ref()
+                .map(|adj| adj.value())
+                .unwrap_or(-Camera::OVERSHOOT_HORIZONTAL);
+            let vadj_value = self
+                .vadjustment
+                .borrow()
+                .as_ref()
+                .map(|adj| adj.value())
+                .unwrap_or(-Camera::OVERSHOOT_VERTICAL);
+            let widget_size = obj.widget_size();
+            let offset_mins_maxs = obj.engine_ref().camera_offset_mins_maxs();
+
             if let Some(signal_id) = self.connections.borrow_mut().vadjustment.take() {
                 let old_adj = self.vadjustment.borrow().as_ref().unwrap().clone();
                 old_adj.disconnect(signal_id);
@@ -504,13 +542,20 @@ mod imp {
             if let Some(ref vadj) = vadj {
                 let signal_id =
                     vadj.connect_value_changed(clone!(@weak obj as canvas => move |_| {
-                        // this triggers a canvaslayout allocate() call, where the strokes rendering is updated based on some conditions
+                        // this triggers a canvaslayout allocate() call,
+                        // where the camera and content rendering is updated based on some conditions
                         canvas.queue_resize();
                     }));
 
                 self.connections.borrow_mut().vadjustment.replace(signal_id);
             }
             self.vadjustment.replace(vadj);
+
+            obj.configure_adjustments(
+                widget_size,
+                offset_mins_maxs,
+                na::vector![hadj_value, vadj_value],
+            );
         }
     }
 }
@@ -650,9 +695,47 @@ impl RnCanvas {
 
     pub(crate) fn canvas_layout_manager(&self) -> RnCanvasLayout {
         self.layout_manager()
+            .and_downcast::<RnCanvasLayout>()
             .unwrap()
-            .downcast::<RnCanvasLayout>()
-            .unwrap()
+    }
+
+    pub(crate) fn configure_adjustments(
+        &self,
+        widget_size: na::Vector2<f64>,
+        offset_mins_maxs: (na::Vector2<f64>, na::Vector2<f64>),
+        offset: na::Vector2<f64>,
+    ) {
+        let (offset_mins, offset_maxs) = offset_mins_maxs;
+
+        if let Some(hadj) = self.hadjustment() {
+            hadj.configure(
+                // This gets clamped to the lower and upper values
+                offset[0],
+                offset_mins[0],
+                offset_maxs[0],
+                0.1 * widget_size[0],
+                0.9 * widget_size[0],
+                widget_size[0],
+            )
+        };
+
+        if let Some(vadj) = self.vadjustment() {
+            vadj.configure(
+                // This gets clamped to the lower and upper values
+                offset[1],
+                offset_mins[1],
+                offset_maxs[1],
+                0.1 * widget_size[1],
+                0.9 * widget_size[1],
+                widget_size[1],
+            );
+        }
+
+        self.queue_resize();
+    }
+
+    pub(crate) fn widget_size(&self) -> na::Vector2<f64> {
+        na::vector![self.width() as f64, self.height() as f64]
     }
 
     /// Immutable borrow of the engine.


Test patch:
diff --git a/misc/file-tests/v0-9-0-test.rnote b/misc/file-tests/v0-9-0-test.rnote
new file mode 100644
index 0000000000..6e5b49e74d
Binary files /dev/null and b/misc/file-tests/v0-9-0-test.rnote differ