feat(viewer): add bounding-box and animated SVG loading spinner
This commit is contained in:
62
Cargo.lock
generated
62
Cargo.lock
generated
@@ -677,6 +677,22 @@ dependencies = [
|
|||||||
"piper",
|
"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]]
|
[[package]]
|
||||||
name = "bstr"
|
name = "bstr"
|
||||||
version = "1.12.0"
|
version = "1.12.0"
|
||||||
@@ -983,6 +999,12 @@ dependencies = [
|
|||||||
"unicode-width",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "color"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a18ef4657441fb193b65f34dc39b3781f0dfec23d3bd94d0eeb4e88cde421edb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "color_quant"
|
name = "color_quant"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -3153,6 +3175,7 @@ name = "mm"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ash",
|
"ash",
|
||||||
|
"bounding-box",
|
||||||
"clap",
|
"clap",
|
||||||
"clap_complete",
|
"clap_complete",
|
||||||
"error-stack",
|
"error-stack",
|
||||||
@@ -3255,6 +3278,21 @@ dependencies = [
|
|||||||
"getrandom 0.2.16",
|
"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]]
|
[[package]]
|
||||||
name = "new_debug_unreachable"
|
name = "new_debug_unreachable"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
@@ -3600,6 +3638,15 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
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]]
|
[[package]]
|
||||||
name = "ordered-stream"
|
name = "ordered-stream"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -3787,6 +3834,21 @@ version = "0.2.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7"
|
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]]
|
[[package]]
|
||||||
name = "postage"
|
name = "postage"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
|||||||
@@ -14,10 +14,14 @@ thiserror = "2.0"
|
|||||||
tokio = "1.43.1"
|
tokio = "1.43.1"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = "0.3"
|
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"
|
nalgebra = "0.34.1"
|
||||||
wayland-sys = { version = "0.31.7", default-features = false }
|
wayland-sys = { version = "0.31.7", default-features = false }
|
||||||
wayland-backend = { version = "0.3.11", default-features = false }
|
wayland-backend = { version = "0.3.11", default-features = false }
|
||||||
ignore = { version = "0.4.23", features = ["simd-accel"] }
|
ignore = { version = "0.4.23", features = ["simd-accel"] }
|
||||||
unicode-segmentation = "1.12.0"
|
unicode-segmentation = "1.12.0"
|
||||||
ash = { version = "0.38.0", features = ["linked"] }
|
ash = { version = "0.38.0", features = ["linked"] }
|
||||||
|
|
||||||
|
bounding-box = { git = "https://github.com/aftershootco/ndcv-bridge" }
|
||||||
|
|||||||
6
assets/arrow_circle.svg
Normal file
6
assets/arrow_circle.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3 8C3 6.67392 3.52678 5.40215 4.46446 4.46447C5.40214 3.52679 6.67391 3.00001 7.99999 3.00001C9.39779 3.00527 10.7394 3.55069 11.7444 4.52223L13 5.77778" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M13 3.00001V5.77778H10.2222" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M13 8C13 9.32608 12.4732 10.5978 11.5355 11.5355C10.5978 12.4732 9.32607 13 7.99999 13C6.60219 12.9947 5.26054 12.4493 4.25555 11.4778L3 10.2222" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M5.77777 10.2222H3V13" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 748 B |
10
src/main.rs
10
src/main.rs
@@ -18,11 +18,11 @@ pub fn main() -> Result<()> {
|
|||||||
.change_context(Error)
|
.change_context(Error)
|
||||||
.attach("Failed to canonicalize path")?;
|
.attach("Failed to canonicalize path")?;
|
||||||
let files = walker(&input);
|
let files = walker(&input);
|
||||||
if files.is_empty() {
|
// if files.is_empty() {
|
||||||
return Err(Error)
|
// return Err(Error)
|
||||||
.attach("No files found in the folder")
|
// .attach("No files found in the folder")
|
||||||
.attach(input.display().to_string());
|
// .attach(input.display().to_string());
|
||||||
}
|
// }
|
||||||
viewer::run(files);
|
viewer::run(files);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
116
src/viewer.rs
116
src/viewer.rs
@@ -1,21 +1,54 @@
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
App, Application, Bounds, Context, FocusHandle, KeyBinding, ObjectFit, Window, WindowBounds,
|
Animation, AnimationExt, App, Application, Bounds, Canvas, Context, FocusHandle, KeyBinding,
|
||||||
WindowOptions, actions, div, img, prelude::*, px, rgb, size,
|
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 nalgebra::Vector2;
|
||||||
use std::path::PathBuf;
|
use std::{path::PathBuf, time::Duration};
|
||||||
|
|
||||||
struct MMViewer {
|
struct MMViewer {
|
||||||
files: Vec<PathBuf>,
|
files: Vec<PathBuf>,
|
||||||
current: usize,
|
current: usize,
|
||||||
zoom: f32,
|
last: Option<usize>,
|
||||||
pan: Vector2<f32>,
|
// zoom: f32,
|
||||||
|
// pan: Vector2<f32>,
|
||||||
focus: FocusHandle,
|
focus: FocusHandle,
|
||||||
fit: ObjectFit,
|
fit: ObjectFit,
|
||||||
}
|
}
|
||||||
|
|
||||||
actions!(mm, [Quit, NextImage, PrevImage, FirstImage, LastImage, Fit]);
|
actions!(mm, [Quit, NextImage, PrevImage, FirstImage, LastImage, Fit]);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct BoundingBox {
|
||||||
|
inner: bounding_box::Aabb2<f32>,
|
||||||
|
pub width: gpui::Pixels,
|
||||||
|
pub color: gpui::Rgba,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<bounding_box::Aabb2<f32>> for BoundingBox {
|
||||||
|
fn from(aabb: bounding_box::Aabb2<f32>) -> 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<Self>) -> 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 {
|
impl Render for MMViewer {
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
div()
|
div()
|
||||||
@@ -31,34 +64,60 @@ impl Render for MMViewer {
|
|||||||
.size_full()
|
.size_full()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.bg(rgb(0x505050))
|
.bg(rgba(0xffffffff))
|
||||||
.child(if let Some(file) = self.files.get(self.current) {
|
.child(if let Some(file) = self.files.get(self.current) {
|
||||||
div().flex().flex_row().size_full().justify_center().child(
|
div()
|
||||||
img(file.clone())
|
.flex()
|
||||||
.object_fit(self.fit())
|
.flex_row()
|
||||||
.size_full()
|
.size_full()
|
||||||
.with_loading(|| {
|
.justify_center()
|
||||||
div()
|
.child(
|
||||||
.flex()
|
img(file.clone())
|
||||||
.flex_row()
|
.id("loupe")
|
||||||
.size_full()
|
.object_fit(self.fit())
|
||||||
.child("Loading...")
|
.size_full()
|
||||||
.bg(rgb(0xffffff))
|
.with_loading(|| Self::loading().into_any_element()),
|
||||||
.justify_center()
|
)
|
||||||
.into_any()
|
.relative()
|
||||||
}),
|
.into_any()
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
div().child(format!(
|
Self::loading().into_any_element()
|
||||||
"No image found (index: {}, total: {})",
|
// div().child(format!(
|
||||||
self.current,
|
// "No image found (index: {}, total: {})",
|
||||||
self.files.len()
|
// 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 {
|
impl MMViewer {
|
||||||
|
fn loading() -> impl IntoElement {
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
|
.flex_none()
|
||||||
|
.bg(rgb(0xff00ff))
|
||||||
|
.child(wheel())
|
||||||
|
.debug()
|
||||||
|
}
|
||||||
|
|
||||||
fn focus_handle(&self, _: &App) -> FocusHandle {
|
fn focus_handle(&self, _: &App) -> FocusHandle {
|
||||||
self.focus.clone()
|
self.focus.clone()
|
||||||
}
|
}
|
||||||
@@ -143,8 +202,9 @@ pub fn run(files: Vec<PathBuf>) {
|
|||||||
cx.new(|cx| MMViewer {
|
cx.new(|cx| MMViewer {
|
||||||
files,
|
files,
|
||||||
current: 0,
|
current: 0,
|
||||||
zoom: 1.0,
|
last: None,
|
||||||
pan: Vector2::new(0.0, 0.0),
|
// zoom: 1.0,
|
||||||
|
// pan: Vector2::new(0.0, 0.0),
|
||||||
focus: cx.focus_handle(),
|
focus: cx.focus_handle(),
|
||||||
fit: ObjectFit::Contain,
|
fit: ObjectFit::Contain,
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user