From 9384b5039729a1fe1a121a3b4731da3ea7f55554 Mon Sep 17 00:00:00 2001 From: uttarayan21 Date: Thu, 9 Oct 2025 13:35:56 +0530 Subject: [PATCH] feat(viewer): add bounding-box and animated SVG loading spinner --- Cargo.lock | 62 +++++++++++++++++++++ Cargo.toml | 6 ++- assets/arrow_circle.svg | 6 +++ src/main.rs | 10 ++-- src/viewer.rs | 116 ++++++++++++++++++++++++++++++---------- 5 files changed, 166 insertions(+), 34 deletions(-) create mode 100644 assets/arrow_circle.svg diff --git a/Cargo.lock b/Cargo.lock index 8d598be..9fe23d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -677,6 +677,22 @@ dependencies = [ "piper", ] +[[package]] +name = "bounding-box" +version = "0.1.0" +source = "git+https://github.com/aftershootco/ndcv-bridge#16c3b2e9105d3528a6ffbc2e1fe9046cb6930a6e" +dependencies = [ + "color", + "itertools 0.14.0", + "nalgebra", + "ndarray", + "num", + "ordered-float", + "simba", + "thiserror 2.0.17", + "tracing", +] + [[package]] name = "bstr" version = "1.12.0" @@ -983,6 +999,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "color" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18ef4657441fb193b65f34dc39b3781f0dfec23d3bd94d0eeb4e88cde421edb" + [[package]] name = "color_quant" version = "1.1.0" @@ -3153,6 +3175,7 @@ name = "mm" version = "0.1.0" dependencies = [ "ash", + "bounding-box", "clap", "clap_complete", "error-stack", @@ -3255,6 +3278,21 @@ dependencies = [ "getrandom 0.2.16", ] +[[package]] +name = "ndarray" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -3600,6 +3638,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-float" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -3787,6 +3834,21 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "postage" version = "0.5.0" diff --git a/Cargo.toml b/Cargo.toml index 057d116..02d0962 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,10 +14,14 @@ thiserror = "2.0" tokio = "1.43.1" tracing = "0.1" tracing-subscriber = "0.3" -gpui = { git = "https://github.com/uttarayan21/zed", default-features = false, features = ["wayland"] } +gpui = { git = "https://github.com/uttarayan21/zed", default-features = false, features = [ + "wayland", +] } nalgebra = "0.34.1" wayland-sys = { version = "0.31.7", default-features = false } wayland-backend = { version = "0.3.11", default-features = false } ignore = { version = "0.4.23", features = ["simd-accel"] } unicode-segmentation = "1.12.0" ash = { version = "0.38.0", features = ["linked"] } + +bounding-box = { git = "https://github.com/aftershootco/ndcv-bridge" } diff --git a/assets/arrow_circle.svg b/assets/arrow_circle.svg new file mode 100644 index 0000000..90e352b --- /dev/null +++ b/assets/arrow_circle.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/main.rs b/src/main.rs index 6a0b14e..07f2b48 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,11 +18,11 @@ pub fn main() -> Result<()> { .change_context(Error) .attach("Failed to canonicalize path")?; let files = walker(&input); - if files.is_empty() { - return Err(Error) - .attach("No files found in the folder") - .attach(input.display().to_string()); - } + // if files.is_empty() { + // return Err(Error) + // .attach("No files found in the folder") + // .attach(input.display().to_string()); + // } viewer::run(files); Ok(()) diff --git a/src/viewer.rs b/src/viewer.rs index 2e38216..544ffbe 100644 --- a/src/viewer.rs +++ b/src/viewer.rs @@ -1,21 +1,54 @@ use gpui::{ - App, Application, Bounds, Context, FocusHandle, KeyBinding, ObjectFit, Window, WindowBounds, - WindowOptions, actions, div, img, prelude::*, px, rgb, size, + Animation, AnimationExt, App, Application, Bounds, Canvas, Context, FocusHandle, KeyBinding, + ObjectFit, Transformation, Window, WindowBounds, WindowOptions, actions, black, bounce, canvas, + div, ease_in_out, img, percentage, prelude::*, pulsating_between, px, rgb, rgba, size, svg, }; use nalgebra::Vector2; -use std::path::PathBuf; +use std::{path::PathBuf, time::Duration}; struct MMViewer { files: Vec, current: usize, - zoom: f32, - pan: Vector2, + last: Option, + // zoom: f32, + // pan: Vector2, focus: FocusHandle, fit: ObjectFit, } actions!(mm, [Quit, NextImage, PrevImage, FirstImage, LastImage, Fit]); +#[derive(Debug, Clone)] +pub struct BoundingBox { + inner: bounding_box::Aabb2, + pub width: gpui::Pixels, + pub color: gpui::Rgba, +} + +impl From> for BoundingBox { + fn from(aabb: bounding_box::Aabb2) -> Self { + Self { + inner: aabb, + width: px(2.0), + color: gpui::green().to_rgb(), + } + } +} + +// impl Render for BoundingBox { +// fn render(&mut self, window: &mut Window, _cx: &mut Context) -> impl IntoElement { +// let mut builder = gpui::PathBuilder::stroke(self.width); +// let aabb = self.inner; +// builder.move_to(gpui::Point::new(px(aabb.x1()), px(aabb.y1()))); +// builder.line_to(gpui::Point::new(px(aabb.x2()), px(aabb.y1()))); +// builder.line_to(gpui::Point::new(px(aabb.x2()), px(aabb.y2()))); +// builder.line_to(gpui::Point::new(px(aabb.x1()), px(aabb.y2()))); +// builder.line_to(gpui::Point::new(px(aabb.x1()), px(aabb.y1()))); +// let path = builder.build().expect("Failed to build path"); +// window.paint_path(path, gpui::green()) +// } +// } + impl Render for MMViewer { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { div() @@ -31,34 +64,60 @@ impl Render for MMViewer { .size_full() .flex_col() .justify_center() - .bg(rgb(0x505050)) + .bg(rgba(0xffffffff)) .child(if let Some(file) = self.files.get(self.current) { - div().flex().flex_row().size_full().justify_center().child( - img(file.clone()) - .object_fit(self.fit()) - .size_full() - .with_loading(|| { - div() - .flex() - .flex_row() - .size_full() - .child("Loading...") - .bg(rgb(0xffffff)) - .justify_center() - .into_any() - }), - ) + div() + .flex() + .flex_row() + .size_full() + .justify_center() + .child( + img(file.clone()) + .id("loupe") + .object_fit(self.fit()) + .size_full() + .with_loading(|| Self::loading().into_any_element()), + ) + .relative() + .into_any() } else { - div().child(format!( - "No image found (index: {}, total: {})", - self.current, - self.files.len() - )) + Self::loading().into_any_element() + // div().child(format!( + // "No image found (index: {}, total: {})", + // self.current, + // self.files.len() + // )) }) } } +const ARROW_CIRCLE_SVG: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/arrow_circle.svg"); + +fn wheel() -> impl IntoElement { + svg() + .path(ARROW_CIRCLE_SVG) + .flex_none() + .size_full() + .text_color(black()) + .with_animation( + "wheel", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(bounce(ease_in_out)), + move |svg, delta| svg.with_transformation(Transformation::rotate(percentage(delta))), + ) +} + impl MMViewer { + fn loading() -> impl IntoElement { + div() + .size_full() + .flex_none() + .bg(rgb(0xff00ff)) + .child(wheel()) + .debug() + } + fn focus_handle(&self, _: &App) -> FocusHandle { self.focus.clone() } @@ -143,8 +202,9 @@ pub fn run(files: Vec) { cx.new(|cx| MMViewer { files, current: 0, - zoom: 1.0, - pan: Vector2::new(0.0, 0.0), + last: None, + // zoom: 1.0, + // pan: Vector2::new(0.0, 0.0), focus: cx.focus_handle(), fit: ObjectFit::Contain, })