231 lines
8.9 KiB
Rust
231 lines
8.9 KiB
Rust
use egui::ImageFit;
|
|
use egui::Slider;
|
|
use egui::Vec2;
|
|
use egui::emath::Rot2;
|
|
use egui::panel::Side;
|
|
use egui::panel::TopBottomSide;
|
|
|
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
|
pub struct ImageViewer {
|
|
current_uri: String,
|
|
uri_edit_text: String,
|
|
image_options: egui::ImageOptions,
|
|
chosen_fit: ChosenFit,
|
|
fit: ImageFit,
|
|
maintain_aspect_ratio: bool,
|
|
max_size: Vec2,
|
|
alt_text: String,
|
|
}
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
|
enum ChosenFit {
|
|
ExactSize,
|
|
Fraction,
|
|
OriginalSize,
|
|
}
|
|
|
|
impl ChosenFit {
|
|
fn as_str(&self) -> &'static str {
|
|
match self {
|
|
Self::ExactSize => "exact size",
|
|
Self::Fraction => "fraction",
|
|
Self::OriginalSize => "original size",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for ImageViewer {
|
|
fn default() -> Self {
|
|
Self {
|
|
current_uri: "https://picsum.photos/seed/1.759706314/1024".to_owned(),
|
|
uri_edit_text: "https://picsum.photos/seed/1.759706314/1024".to_owned(),
|
|
image_options: egui::ImageOptions::default(),
|
|
chosen_fit: ChosenFit::Fraction,
|
|
fit: ImageFit::Fraction(Vec2::splat(1.0)),
|
|
maintain_aspect_ratio: true,
|
|
max_size: Vec2::splat(2048.0),
|
|
alt_text: "My Image".to_owned(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl eframe::App for ImageViewer {
|
|
fn update(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) {
|
|
egui::TopBottomPanel::new(TopBottomSide::Top, "url bar").show(ctx, |ui| {
|
|
ui.horizontal_centered(|ui| {
|
|
let label = ui.label("URI:");
|
|
ui.text_edit_singleline(&mut self.uri_edit_text)
|
|
.labelled_by(label.id);
|
|
if ui.small_button("✔").clicked() {
|
|
ctx.forget_image(&self.current_uri);
|
|
self.uri_edit_text = self.uri_edit_text.trim().to_owned();
|
|
self.current_uri = self.uri_edit_text.clone();
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
if ui.button("file…").clicked()
|
|
&& let Some(path) = rfd::FileDialog::new().pick_file()
|
|
{
|
|
self.uri_edit_text = format!("file://{}", path.display());
|
|
self.current_uri = self.uri_edit_text.clone();
|
|
}
|
|
});
|
|
});
|
|
|
|
egui::SidePanel::new(Side::Left, "controls").show(ctx, |ui| {
|
|
// uv
|
|
ui.label("UV");
|
|
ui.add(Slider::new(&mut self.image_options.uv.min.x, 0.0..=1.0).text("min x"));
|
|
ui.add(Slider::new(&mut self.image_options.uv.min.y, 0.0..=1.0).text("min y"));
|
|
ui.add(Slider::new(&mut self.image_options.uv.max.x, 0.0..=1.0).text("max x"));
|
|
ui.add(Slider::new(&mut self.image_options.uv.max.y, 0.0..=1.0).text("max y"));
|
|
|
|
// rotation
|
|
ui.add_space(2.0);
|
|
let had_rotation = self.image_options.rotation.is_some();
|
|
let mut has_rotation = had_rotation;
|
|
ui.checkbox(&mut has_rotation, "Rotation");
|
|
match (had_rotation, has_rotation) {
|
|
(true, false) => self.image_options.rotation = None,
|
|
(false, true) => {
|
|
self.image_options.rotation =
|
|
Some((Rot2::from_angle(0.0), Vec2::new(0.5, 0.5)));
|
|
}
|
|
(true, true) | (false, false) => {}
|
|
}
|
|
|
|
if let Some((rot, origin)) = self.image_options.rotation.as_mut() {
|
|
let mut angle = rot.angle();
|
|
|
|
ui.label("angle");
|
|
ui.drag_angle(&mut angle);
|
|
*rot = Rot2::from_angle(angle);
|
|
|
|
ui.add(Slider::new(&mut origin.x, 0.0..=1.0).text("origin x"));
|
|
ui.add(Slider::new(&mut origin.y, 0.0..=1.0).text("origin y"));
|
|
}
|
|
|
|
// bg_fill
|
|
ui.add_space(2.0);
|
|
ui.horizontal(|ui| {
|
|
ui.color_edit_button_srgba(&mut self.image_options.bg_fill);
|
|
ui.label("Background color");
|
|
});
|
|
|
|
// tint
|
|
ui.add_space(2.0);
|
|
ui.horizontal(|ui| {
|
|
ui.color_edit_button_srgba(&mut self.image_options.tint);
|
|
ui.label("Tint");
|
|
});
|
|
|
|
// fit
|
|
ui.add_space(10.0);
|
|
ui.label(
|
|
"The chosen fit will determine how the image tries to fill the available space",
|
|
);
|
|
egui::ComboBox::from_label("Fit")
|
|
.selected_text(self.chosen_fit.as_str())
|
|
.show_ui(ui, |ui| {
|
|
ui.selectable_value(
|
|
&mut self.chosen_fit,
|
|
ChosenFit::ExactSize,
|
|
ChosenFit::ExactSize.as_str(),
|
|
);
|
|
ui.selectable_value(
|
|
&mut self.chosen_fit,
|
|
ChosenFit::Fraction,
|
|
ChosenFit::Fraction.as_str(),
|
|
);
|
|
ui.selectable_value(
|
|
&mut self.chosen_fit,
|
|
ChosenFit::OriginalSize,
|
|
ChosenFit::OriginalSize.as_str(),
|
|
);
|
|
});
|
|
|
|
match self.chosen_fit {
|
|
ChosenFit::ExactSize => {
|
|
if !matches!(self.fit, ImageFit::Exact(_)) {
|
|
self.fit = ImageFit::Exact(Vec2::splat(128.0));
|
|
}
|
|
let ImageFit::Exact(size) = &mut self.fit else {
|
|
unreachable!()
|
|
};
|
|
ui.add(Slider::new(&mut size.x, 0.0..=2048.0).text("width"));
|
|
ui.add(Slider::new(&mut size.y, 0.0..=2048.0).text("height"));
|
|
}
|
|
ChosenFit::Fraction => {
|
|
if !matches!(self.fit, ImageFit::Fraction(_)) {
|
|
self.fit = ImageFit::Fraction(Vec2::splat(1.0));
|
|
}
|
|
let ImageFit::Fraction(fract) = &mut self.fit else {
|
|
unreachable!()
|
|
};
|
|
ui.add(Slider::new(&mut fract.x, 0.0..=1.0).text("width"));
|
|
ui.add(Slider::new(&mut fract.y, 0.0..=1.0).text("height"));
|
|
}
|
|
ChosenFit::OriginalSize => {
|
|
if !matches!(self.fit, ImageFit::Original { .. }) {
|
|
self.fit = ImageFit::Original { scale: 1.0 };
|
|
}
|
|
let ImageFit::Original { scale } = &mut self.fit else {
|
|
unreachable!()
|
|
};
|
|
ui.add(Slider::new(scale, 0.1..=4.0).text("scale"));
|
|
}
|
|
}
|
|
|
|
// max size
|
|
ui.add_space(5.0);
|
|
ui.label("The calculated size will not exceed the maximum size");
|
|
ui.add(Slider::new(&mut self.max_size.x, 0.0..=2048.0).text("width"));
|
|
ui.add(Slider::new(&mut self.max_size.y, 0.0..=2048.0).text("height"));
|
|
|
|
// aspect ratio
|
|
ui.add_space(5.0);
|
|
ui.label("Aspect ratio is maintained by scaling both sides as necessary");
|
|
ui.checkbox(&mut self.maintain_aspect_ratio, "Maintain aspect ratio");
|
|
|
|
// alt text
|
|
ui.add_space(5.0);
|
|
ui.label("Alt text");
|
|
ui.text_edit_singleline(&mut self.alt_text);
|
|
|
|
// forget all images
|
|
if ui.button("Forget all images").clicked() {
|
|
ui.ctx().forget_all_images();
|
|
}
|
|
});
|
|
|
|
egui::CentralPanel::default().show(ctx, |ui| {
|
|
egui::ScrollArea::both().show(ui, |ui| {
|
|
let mut image = egui::Image::from_uri(&self.current_uri);
|
|
image = image.uv(self.image_options.uv);
|
|
image = image.bg_fill(self.image_options.bg_fill);
|
|
image = image.tint(self.image_options.tint);
|
|
let (angle, origin) = self
|
|
.image_options
|
|
.rotation
|
|
.map_or((0.0, Vec2::splat(0.5)), |(rot, origin)| {
|
|
(rot.angle(), origin)
|
|
});
|
|
image = image.rotate(angle, origin);
|
|
match self.fit {
|
|
ImageFit::Original { scale } => image = image.fit_to_original_size(scale),
|
|
ImageFit::Fraction(fract) => image = image.fit_to_fraction(fract),
|
|
ImageFit::Exact(size) => image = image.fit_to_exact_size(size),
|
|
}
|
|
image = image.maintain_aspect_ratio(self.maintain_aspect_ratio);
|
|
image = image.max_size(self.max_size);
|
|
if !self.alt_text.is_empty() {
|
|
image = image.alt_text(&self.alt_text);
|
|
}
|
|
|
|
ui.add_sized(ui.available_size(), image);
|
|
});
|
|
});
|
|
}
|
|
}
|