initial commit
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I4a6b498153eccd5407510dd541b7f4816a6a6964
This commit is contained in:
commit
6a73d11c4b
124 changed files with 34856 additions and 0 deletions
236
crates/pinakes-ui/src/components/image_viewer.rs
Normal file
236
crates/pinakes-ui/src/components/image_viewer.rs
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
use dioxus::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum FitMode {
|
||||
FitScreen,
|
||||
FitWidth,
|
||||
Actual,
|
||||
}
|
||||
|
||||
impl FitMode {
|
||||
fn next(self) -> Self {
|
||||
match self {
|
||||
Self::FitScreen => Self::FitWidth,
|
||||
Self::FitWidth => Self::Actual,
|
||||
Self::Actual => Self::FitScreen,
|
||||
}
|
||||
}
|
||||
|
||||
fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::FitScreen => "Fit",
|
||||
Self::FitWidth => "Width",
|
||||
Self::Actual => "100%",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ImageViewer(
|
||||
src: String,
|
||||
alt: String,
|
||||
on_close: EventHandler<()>,
|
||||
#[props(default)] on_prev: Option<EventHandler<()>>,
|
||||
#[props(default)] on_next: Option<EventHandler<()>>,
|
||||
) -> Element {
|
||||
let mut zoom = use_signal(|| 1.0f64);
|
||||
let mut offset_x = use_signal(|| 0.0f64);
|
||||
let mut offset_y = use_signal(|| 0.0f64);
|
||||
let mut dragging = use_signal(|| false);
|
||||
let mut drag_start_x = use_signal(|| 0.0f64);
|
||||
let mut drag_start_y = use_signal(|| 0.0f64);
|
||||
let mut fit_mode = use_signal(|| FitMode::FitScreen);
|
||||
|
||||
let z = *zoom.read();
|
||||
let ox = *offset_x.read();
|
||||
let oy = *offset_y.read();
|
||||
let is_dragging = *dragging.read();
|
||||
let zoom_pct = (z * 100.0) as u32;
|
||||
let current_fit = *fit_mode.read();
|
||||
|
||||
let transform = format!("translate({ox}px, {oy}px) scale({z})");
|
||||
let cursor = if z > 1.0 {
|
||||
if is_dragging { "grabbing" } else { "grab" }
|
||||
} else {
|
||||
"default"
|
||||
};
|
||||
|
||||
// Compute image style based on fit mode
|
||||
let img_style = match current_fit {
|
||||
FitMode::FitScreen => format!(
|
||||
"transform: {transform}; cursor: {cursor}; max-width: 100%; max-height: 100%; object-fit: contain;"
|
||||
),
|
||||
FitMode::FitWidth => {
|
||||
format!("transform: {transform}; cursor: {cursor}; width: 100%; object-fit: contain;")
|
||||
}
|
||||
FitMode::Actual => format!("transform: {transform}; cursor: {cursor};"),
|
||||
};
|
||||
|
||||
let on_wheel = move |e: WheelEvent| {
|
||||
e.prevent_default();
|
||||
let delta = e.delta().strip_units();
|
||||
let factor = if delta.y < 0.0 { 1.1 } else { 1.0 / 1.1 };
|
||||
let new_zoom = (*zoom.read() * factor).clamp(0.1, 20.0);
|
||||
zoom.set(new_zoom);
|
||||
};
|
||||
|
||||
let on_mouse_down = move |e: MouseEvent| {
|
||||
if *zoom.read() > 1.0 {
|
||||
dragging.set(true);
|
||||
let coords = e.client_coordinates();
|
||||
drag_start_x.set(coords.x - *offset_x.read());
|
||||
drag_start_y.set(coords.y - *offset_y.read());
|
||||
}
|
||||
};
|
||||
|
||||
let on_mouse_move = move |e: MouseEvent| {
|
||||
if *dragging.read() {
|
||||
let coords = e.client_coordinates();
|
||||
offset_x.set(coords.x - *drag_start_x.read());
|
||||
offset_y.set(coords.y - *drag_start_y.read());
|
||||
}
|
||||
};
|
||||
|
||||
let on_mouse_up = move |_: MouseEvent| {
|
||||
dragging.set(false);
|
||||
};
|
||||
|
||||
let on_keydown = {
|
||||
move |evt: KeyboardEvent| match evt.key() {
|
||||
Key::Escape => on_close.call(()),
|
||||
Key::Character(ref c) if c == "+" || c == "=" => {
|
||||
let new_zoom = (*zoom.read() * 1.2).min(20.0);
|
||||
zoom.set(new_zoom);
|
||||
}
|
||||
Key::Character(ref c) if c == "-" => {
|
||||
let new_zoom = (*zoom.read() / 1.2).max(0.1);
|
||||
zoom.set(new_zoom);
|
||||
}
|
||||
Key::Character(ref c) if c == "0" => {
|
||||
zoom.set(1.0);
|
||||
offset_x.set(0.0);
|
||||
offset_y.set(0.0);
|
||||
fit_mode.set(FitMode::FitScreen);
|
||||
}
|
||||
Key::ArrowLeft => {
|
||||
if let Some(ref prev) = on_prev {
|
||||
prev.call(());
|
||||
zoom.set(1.0);
|
||||
offset_x.set(0.0);
|
||||
offset_y.set(0.0);
|
||||
}
|
||||
}
|
||||
Key::ArrowRight => {
|
||||
if let Some(ref next) = on_next {
|
||||
next.call(());
|
||||
zoom.set(1.0);
|
||||
offset_x.set(0.0);
|
||||
offset_y.set(0.0);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
};
|
||||
|
||||
let zoom_in = move |_| {
|
||||
let new_zoom = (*zoom.read() * 1.2).min(20.0);
|
||||
zoom.set(new_zoom);
|
||||
};
|
||||
|
||||
let zoom_out = move |_| {
|
||||
let new_zoom = (*zoom.read() / 1.2).max(0.1);
|
||||
zoom.set(new_zoom);
|
||||
};
|
||||
|
||||
let cycle_fit = move |_| {
|
||||
let next = fit_mode.read().next();
|
||||
fit_mode.set(next);
|
||||
zoom.set(1.0);
|
||||
offset_x.set(0.0);
|
||||
offset_y.set(0.0);
|
||||
};
|
||||
|
||||
let has_prev = on_prev.is_some();
|
||||
let has_next = on_next.is_some();
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: "image-viewer-overlay",
|
||||
tabindex: "0",
|
||||
onkeydown: on_keydown,
|
||||
|
||||
// Toolbar
|
||||
div { class: "image-viewer-toolbar",
|
||||
div { class: "image-viewer-toolbar-left",
|
||||
if has_prev {
|
||||
button {
|
||||
class: "iv-btn",
|
||||
onclick: move |_| {
|
||||
if let Some(ref prev) = on_prev {
|
||||
prev.call(());
|
||||
zoom.set(1.0);
|
||||
offset_x.set(0.0);
|
||||
offset_y.set(0.0);
|
||||
}
|
||||
},
|
||||
title: "Previous",
|
||||
"\u{25c0}"
|
||||
}
|
||||
}
|
||||
if has_next {
|
||||
button {
|
||||
class: "iv-btn",
|
||||
onclick: move |_| {
|
||||
if let Some(ref next) = on_next {
|
||||
next.call(());
|
||||
zoom.set(1.0);
|
||||
offset_x.set(0.0);
|
||||
offset_y.set(0.0);
|
||||
}
|
||||
},
|
||||
title: "Next",
|
||||
"\u{25b6}"
|
||||
}
|
||||
}
|
||||
}
|
||||
div { class: "image-viewer-toolbar-center",
|
||||
button { class: "iv-btn", onclick: cycle_fit, title: "Cycle fit mode",
|
||||
"{current_fit.label()}"
|
||||
}
|
||||
button { class: "iv-btn", onclick: zoom_out, title: "Zoom out", "\u{2212}" }
|
||||
span { class: "iv-zoom-label", "{zoom_pct}%" }
|
||||
button { class: "iv-btn", onclick: zoom_in, title: "Zoom in", "+" }
|
||||
}
|
||||
div { class: "image-viewer-toolbar-right",
|
||||
button {
|
||||
class: "iv-btn iv-close",
|
||||
onclick: move |_| on_close.call(()),
|
||||
title: "Close",
|
||||
"\u{2715}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Image canvas
|
||||
div {
|
||||
class: "image-viewer-canvas",
|
||||
onwheel: on_wheel,
|
||||
onmousedown: on_mouse_down,
|
||||
onmousemove: on_mouse_move,
|
||||
onmouseup: on_mouse_up,
|
||||
onclick: move |e: MouseEvent| {
|
||||
// Close on background click (not on image)
|
||||
e.stop_propagation();
|
||||
},
|
||||
|
||||
img {
|
||||
src: "{src}",
|
||||
alt: "{alt}",
|
||||
style: "{img_style}",
|
||||
draggable: "false",
|
||||
onclick: move |e: MouseEvent| e.stop_propagation(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue