Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
4fb43b4
wip: do mini preparation
THEGOLDENPRO Dec 11, 2024
7974ad5
feat, refactor!: use `text_colour` from cirrus, move optimize logic a…
THEGOLDENPRO Dec 17, 2024
f8e8ee2
refactor: modularized more image loading logic
THEGOLDENPRO Dec 17, 2024
49fba8d
refactor: switch to cirrus for egui styling
THEGOLDENPRO Dec 31, 2024
e958ecd
wip: perform large refactor of image optimizations, image loader and …
THEGOLDENPRO Jan 12, 2025
dd7b9c1
wip: perform large refactor of image optimizations, image loader and …
THEGOLDENPRO Jan 12, 2025
41e3e72
feat, refactor: force pan reset too if user zooms out enough, re-add …
THEGOLDENPRO Jan 12, 2025
95df78d
refactor: move some optimization logic from `image.rs` to `optimizati…
THEGOLDENPRO Jan 12, 2025
4a5f0bd
wip: panic so I remember where I left off #25
THEGOLDENPRO Jan 12, 2025
181ba6c
feat: add back monitor downsampling and separate image optimizations …
THEGOLDENPRO Jan 18, 2025
1f3225c
fix: avoid cloning pixels
THEGOLDENPRO Jan 25, 2025
c8f0bc5
refactor: handle image from ImageHandler
THEGOLDENPRO Jan 25, 2025
b92f7d3
chore: add todo
THEGOLDENPRO Jan 25, 2025
4af643d
feat: begin working on actual dynamic sampling implementation #25
THEGOLDENPRO Jan 26, 2025
9638da4
Merge pull request #48 from cloudy-org/main
THEGOLDENPRO Jan 29, 2025
6aa6f16
feat: switch to egui monitor size
THEGOLDENPRO Jan 30, 2025
3e9a60b
refactor: get rid of `display-info` and completely switch over to `Mo…
THEGOLDENPRO Feb 1, 2025
1f51fde
Merge branch 'main' into feat/switch-to-egui-monitor-size
THEGOLDENPRO Mar 15, 2025
c3425b5
Merge branch 'main' into feat/switch-to-egui-monitor-size
THEGOLDENPRO Mar 15, 2025
580a715
wip: add persistent monitor size caching #46
THEGOLDENPRO Mar 15, 2025
37bc839
feat: cache egui viewport monitor size and update cached size when ne…
THEGOLDENPRO Mar 16, 2025
ac89fc9
feat: improve debug logs
THEGOLDENPRO Mar 16, 2025
af1d4d6
feat: use cached monitor size initially. #46
THEGOLDENPRO Mar 19, 2025
222c57e
feat: add `override_monitor_size` config key #46
THEGOLDENPRO Mar 21, 2025
d036957
feat: improve error handling and reporting #46
THEGOLDENPRO Mar 21, 2025
b2c6cde
Merge branch 'feat/dynamic-downsampling' into feat/switch-to-egui-mon…
THEGOLDENPRO Mar 21, 2025
ffdcfac
Merge pull request #49 from cloudy-org/feat/switch-to-egui-monitor-size
THEGOLDENPRO Mar 21, 2025
dc8f0b3
wip: more work on `dynamic_sampling_update` #25
THEGOLDENPRO Mar 21, 2025
1eea29a
feat: implement `accumulated_zoom_factor` and start adding `Scheduler`
THEGOLDENPRO Mar 23, 2025
8508c59
feat: implement upsampling and downsampling scheduling mechanism #25
THEGOLDENPRO Mar 24, 2025
5a6d706
refactor!: move optimizations hash set to ImageHandler fixes #52, add…
THEGOLDENPRO Mar 29, 2025
a04dca6
fix: `Theme::default` is deprecated and accent_colour is no longer an…
THEGOLDENPRO Mar 30, 2025
f1f7e03
chore: add workspace note
THEGOLDENPRO Mar 31, 2025
f335858
Merge branch 'feat/dynamic-downsampling' of goldy.github.com:cloudy-o…
THEGOLDENPRO Mar 31, 2025
52d63c8
fix: rust lifetime compile error, closes #52, begin implementing relo…
THEGOLDENPRO Mar 31, 2025
636d7cd
Merge pull request #54 from cloudy-org/refactor/move-optimizations-ha…
THEGOLDENPRO Mar 31, 2025
6bc3f78
fix, feat: multiple resizes in `image_modifications`, capability to d…
THEGOLDENPRO Apr 1, 2025
352e37f
feat: remove last egui image from memory
THEGOLDENPRO Apr 1, 2025
e46e956
feat: improve logging, fix todo comment
THEGOLDENPRO Apr 2, 2025
7247450
fix: image doesn't upsample when zooming in for the second time
ananasmoe Apr 3, 2025
a2af58f
feat: cloning not needed here
THEGOLDENPRO Apr 3, 2025
f4780a2
feat, chore: setting `dynamic_sampling_old_resolution` to `(0, 0)` is…
THEGOLDENPRO Apr 6, 2025
ac76869
fix: image dynamic sampling not upsampling / downsampling correctly, …
THEGOLDENPRO Apr 17, 2025
ec539d1
Merge branch 'feat/dynamic-downsampling' into fix/upsampling-bug
THEGOLDENPRO Apr 17, 2025
47c4938
Merge pull request #55 from cloudy-org/fix/upsampling-bug
THEGOLDENPRO Apr 17, 2025
24b1517
feat: add `[image.optimizations]` to config, #56
THEGOLDENPRO Apr 18, 2025
095329c
feat, fix: add `use_dynamic_sampling_optimization` field to experimen…
THEGOLDENPRO Apr 18, 2025
69b390a
feat: implement reload mechanism, closes #59 but still wip
THEGOLDENPRO Apr 19, 2025
69c5171
feat: don't reload image if modifications are the same since last rel…
THEGOLDENPRO Apr 20, 2025
13ee2d3
feat, fix: `image.reload_image` should decide whether it should load …
THEGOLDENPRO Apr 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,19 @@ re_format = "0.20.0"
cap = "0.1.2"
clap = {version = "4.5.21", features = ["derive"]}
rand = "0.8.5"
display-info = "0.5.2"
egui-notify = "0.17.0"
svg_metadata = "0.5.1"
textwrap = "0.16.1"
dirs = "5.0.1"
rayon = "1.10.0"
serde_derive = "1.0.215"
serde_json = "1.0"
fs2 = "0.4.3"

toml = {workspace = true}
serde = {workspace = true, features = ["derive"]}

# NOTE: Workspace deps will be removed here when we soon update to the latest commit of cirrus.
[workspace.dependencies]
cirrus_egui = { path = "./cirrus/egui" }
cirrus_theming = { path = "./cirrus/theming" }
Expand Down
42 changes: 37 additions & 5 deletions assets/config.template.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,30 @@ initial.lazy_loading = false
# For example: Picking an image from the file picker or dropping an image into the window.
gui.lazy_loading = true

# Setting this to "false" will make dynamic scaling reload images on the main thread. It's not
# Setting this to "false" will make dynamic sampling reload images on the main thread. It's not
# recommended to set this to "false" as it will lead to the GUI freezing each time you try to zoom in/out.
dynamic.lazy_loading = true

[image.optimizations]
# Comment this out if you would like to customize the optimization settings below.
# The optimizations that are defaulted to in "mode" will always take priority.
mode = "default"

# Downsamples the image roughly to the resolution of your monitor.
#
# Images don't always have to be displayed at their full native resolution, especially when
# the image is significantly bigger than your monitor can even display, so to save memory
# we downsample the image. Downsampling decreases the amount of memory eaten up by the image
# at the cost of CPU time wasted actually resizing the image. The bigger the image the more time
# it will take to downsample but we think memory savings are more valuable in this circumstance.
#
# If you do not wish for such memory savings and you prefer faster image load time disable
# this optimization.
#
# If you want your image quality back when zooming into your image,
# you might want to also enable the "dynamic_sampling" image optimization too.
monitor_downsampling = {enabled = true, strength = 1.3}

[ui]

[ui.magnification_panel]
Expand All @@ -31,20 +51,32 @@ about_box.toggle = "A"
# Keybind to reset the image pan
# position and zoom scale back to default.
image.reset_pos = "R"
# Keybind to toggle all your UI controlls like the magnification panel.
# Keybind to toggle all your UI controls like the magnification panel.
ui_controls.toggle = "C"

[misc]
# All other configs that don't yet have a specific place or are experimental.
# Configs here will be moved or removed without further notice.

# override_monitor_size = {width = 1920, height = 1080}

[misc.experimental]
# Settings to toggle experimental features that aren't yet ready to be
# Settings to toggle experimental features that aren't yet ready yet to be
# used by the wider user audience. This exists for the sole purpose of testing.
#
# Remember these are EXPERIMENTAL, bugs WILL be present.


# Setting this to true will enable the experimental lanczos roseate image processing backend
# which is faster than the image-rs backend but may result in buggy/blocky images.
# Disable this if the image looks weird.
use_fast_roseate_backend = false
use_fast_roseate_backend = false

# Setting this to true will enable the new but extremely experimental
# dynamic sampling feature that upsamples your image when you zoom into it
# to bring back the detail lost from monitor downsampling.
#
# If this feature was more complete it would live under "[image.optimizations]"
# as "dynamic_sampling = {enabled = false, also_downsample = true}".
#
# Again, this is VERY experimental and incomplete! Expect a very broken implementation.
use_dynamic_sampling_optimization = false
161 changes: 69 additions & 92 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -1,34 +1,32 @@
use std::time::Duration;
use std::{hash::{DefaultHasher, Hash, Hasher}, time::Duration};

use cirrus_theming::v1::{Colour, Theme};
use eframe::egui::{self, Align, Color32, Context, CursorIcon, Frame, Layout, Margin, Rect, Shadow, Stroke, Style, TextStyle, Vec2};
use cirrus_theming::v1::Theme;
use eframe::egui::{self, Align, Color32, Context, CursorIcon, Frame, Layout, Margin, Rect, Stroke, Vec2};
use egui_notify::ToastLevel;

use crate::{config::config::Config, files, image::image::Image, image_loader::ImageLoader, windows::info::InfoWindow, magnification_panel::MagnificationPanel, notifier::NotifierAPI, window_scaling::WindowScaling, windows::about::AboutWindow, zoom_pan::ZoomPan};
use crate::{config::config::Config, files, image_handler::{optimization::ImageOptimizations, ImageHandler}, magnification_panel::MagnificationPanel, monitor_size::MonitorSize, notifier::NotifierAPI, window_scaling::WindowScaling, windows::{about::AboutWindow, info::InfoWindow}, zoom_pan::ZoomPan};

pub struct Roseate<'a> {
theme: Theme,
image: Option<Image>,
zoom_pan: ZoomPan,
info_box: InfoWindow,
about_box: AboutWindow<'a>,
notifier: NotifierAPI,
magnification_panel: MagnificationPanel,
window_scaling: WindowScaling,
last_window_rect: Rect,
image_loader: ImageLoader,
image_handler: ImageHandler,
monitor_size: MonitorSize,
config: Config,
}

impl<'a> Roseate<'a> {
pub fn new(image: Option<Image>, theme: Theme, mut notifier: NotifierAPI, config: Config) -> Self {
let mut image_loader = ImageLoader::new();

if image.is_some() {
image_loader.load_image(
&mut image.clone().unwrap(),
config.image.loading.initial.lazy_loading,
pub fn new(mut image_handler: ImageHandler, monitor_size: MonitorSize, mut notifier: NotifierAPI, theme: Theme, config: Config) -> Self {
if image_handler.image.is_some() {
image_handler.load_image(
config.image.loading.initial.lazy_loading,
&mut notifier,
&monitor_size,
config.misc.experimental.use_fast_roseate_backend
);
}
Expand All @@ -39,7 +37,6 @@ impl<'a> Roseate<'a> {
let magnification_panel = MagnificationPanel::new(&config, &mut notifier);

Self {
image,
theme,
notifier,
zoom_pan,
Expand All @@ -48,65 +45,20 @@ impl<'a> Roseate<'a> {
magnification_panel,
window_scaling: WindowScaling::new(&config),
last_window_rect: Rect::NOTHING,
image_loader: image_loader,
monitor_size,
image_handler,
config,
}
}

fn set_app_style(&self, ctx: &Context) {
// #1d0a0a # dark mode secondary colour for roseate
let mut custom_style = Style {
override_text_style: Some(TextStyle::Monospace),
..Default::default()
};

// TODO: override more default
// colours here with colours from our theme.

// Background colour styling.
custom_style.visuals.panel_fill = Color32::from_hex(
&self.theme.primary_colour.hex_code
).unwrap();

// Window styling.
custom_style.visuals.window_highlight_topmost = false;

custom_style.visuals.window_fill = Color32::from_hex(
&self.theme.secondary_colour.hex_code
).unwrap();
custom_style.visuals.window_stroke = Stroke::new(
1.0,
Color32::from_hex(&self.theme.third_colour.hex_code).unwrap()
);
custom_style.visuals.window_shadow = Shadow::NONE;

custom_style.visuals.widgets.inactive.bg_fill =
Color32::from_hex(
&self.theme.primary_colour.hex_code
).unwrap();

// Text styling.
custom_style.visuals.override_text_color = Some(
Color32::from_hex(
match self.theme.is_dark {
true => "#b5b5b5",
false => "#3b3b3b"
}
).unwrap()
);

ctx.set_style(custom_style);
}

fn draw_dotted_line(&self, ui: &egui::Painter, pos: &[egui::Pos2]) {
ui.add(
egui::Shape::dashed_line(
pos,
Stroke {
width: 2.0,
color: Color32::from_hex(
&self.theme.accent_colour.as_ref()
.unwrap_or(&Colour {hex_code: "e05f78".into()}).hex_code
&self.theme.accent_colour.hex_code
).unwrap()
},
10.0,
Expand All @@ -119,9 +71,7 @@ impl<'a> Roseate<'a> {
impl eframe::App for Roseate<'_> {

fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
self.set_app_style(ctx);

self.info_box.init(&self.image);
self.info_box.init(&self.image_handler.image);
self.info_box.handle_input(ctx);

self.zoom_pan.handle_reset_input(ctx);
Expand All @@ -139,10 +89,20 @@ impl eframe::App for Roseate<'_> {
}

self.notifier.update(ctx);
self.monitor_size.update(ctx, &mut self.notifier);
self.about_box.update(ctx); // we update this box here because we want
// the about box is to be toggleable even without an image.

if self.image.is_none() {
if self.image_handler.image.is_none() {
let mut configured_image_optimizations = self.config.image.optimizations.get_optimizations();

// TODO: remove this once we move DS to "[image.optimizations]".
if self.config.misc.experimental.use_dynamic_sampling_optimization {
configured_image_optimizations.push(
ImageOptimizations::DynamicSampling(true, true)
);
}

// Collect dropped files.
ctx.input(|i| {
let dropped_files = &i.raw.dropped_files;
Expand All @@ -153,21 +113,22 @@ impl eframe::App for Roseate<'_> {
.as_ref()
.unwrap(); // gotta love rust ~ ananas

let mut image = match Image::from_path(path) {
Ok(value) => value,
Err(error) => {
self.notifier.toasts.lock().unwrap().toast_and_log(
error.into(), ToastLevel::Error
);
return;
}
};
let result = self.image_handler.init_image(
path,
configured_image_optimizations.clone()
);

self.image = Some(image.clone());
self.image_loader.load_image(
&mut image,
if let Err(error) = result {
self.notifier.toasts.lock().unwrap().toast_and_log(
error.into(), ToastLevel::Error
);
return;
}

self.image_handler.load_image(
true,
&mut self.notifier,
&self.monitor_size,
self.config.misc.experimental.use_fast_roseate_backend
);
}
Expand Down Expand Up @@ -205,16 +166,16 @@ impl eframe::App for Roseate<'_> {
rose_response.clone().on_hover_cursor(CursorIcon::PointingHand);

if rose_response.clicked() {
let image_result = files::select_image();

match image_result {
Ok(mut image) => {
self.image = Some(image.clone());
let result = self.image_handler.select_image(
configured_image_optimizations
);

self.image_loader.load_image(
&mut image,
match result {
Ok(_) => {
self.image_handler.load_image(
self.config.image.loading.gui.lazy_loading,
&mut self.notifier,
&self.monitor_size,
self.config.misc.experimental.use_fast_roseate_backend
);
},
Expand Down Expand Up @@ -250,19 +211,31 @@ impl eframe::App for Roseate<'_> {

self.info_box.update(ctx);
self.zoom_pan.update(ctx);
self.image_loader.update();
self.image_handler.update(
&ctx,
&self.zoom_pan,
&self.monitor_size,
&mut self.notifier,
self.config.misc.experimental.use_fast_roseate_backend
);
self.magnification_panel.update(ctx, &mut self.zoom_pan);

let image = self.image.clone().unwrap();
let image = self.image_handler.image.as_ref().unwrap();

if self.image_loader.image_loaded {
if self.image_handler.image_loaded {
ui.centered_and_justified(|ui| {
let scaled_image_size = self.window_scaling.relative_image_size(
Vec2::new(image.image_size.width as f32, image.image_size.height as f32)
);


// TODO: umm I think we should move this to self.zoom_pan.update()
// and then move that function in here as we need `scaled_image_size`.
if self.zoom_pan.is_pan_out_of_bounds(scaled_image_size) {
self.zoom_pan.schedule_pan_reset(Duration::from_millis(300));

// As resetting the pan will just snap us back to the center
// of the image we might as well schedule a reset for image scale too.
self.zoom_pan.schedule_scale_reset(Duration::from_millis(300));
};

// NOTE: umm do we move this to window scaling... *probably* if we
Expand All @@ -283,12 +256,17 @@ impl eframe::App for Roseate<'_> {

let response = ui.allocate_rect(zoom_pan_rect, egui::Sense::hover());

let mut hasher = DefaultHasher::new();
image.hash(&mut hasher);

egui::Image::from_bytes(
format!(
"bytes://{}", image.image_path.to_string_lossy()
"bytes://{}", hasher.finish() // so we can indicate to egui that this is a different image when it is modified.
),
// we can unwrap because we know the bytes exist thanks to 'self.image_loader.image_loaded'.
image.image_bytes.lock().unwrap().clone().unwrap()
image.image_bytes.lock().unwrap().clone().expect(
"Image bytes went 'None' when image was still in loaded state in the image handler."
)
).rounding(10.0)
.paint_at(ui, zoom_pan_rect);

Expand Down Expand Up @@ -334,5 +312,4 @@ impl eframe::App for Roseate<'_> {
);

}

}
3 changes: 3 additions & 0 deletions src/config/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ impl Config {

let roseate_config_dir_path = local_config_dir.join("cloudy").join("roseate");

// TODO: switch to get_local_config_path function.
// let roseate_config_path = files::get_local_config_path()?;

if !roseate_config_dir_path.exists() {
debug!("Creating config directory for roseate...");
if let Err(err) = fs::create_dir_all(&roseate_config_dir_path) {
Expand Down
Loading