mirror of
https://github.com/vee1e/moomer.git
synced 2026-09-01 10:18:47 +00:00
Initial commit
This commit is contained in:
commit
9d9efab167
7 changed files with 2699 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
target/release
|
||||
2431
Cargo.lock
generated
Normal file
2431
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
11
Cargo.toml
Normal file
11
Cargo.toml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "moomer"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
screenshots = "0.8"
|
||||
winit = "0.28"
|
||||
pixels = "0.13"
|
||||
image = "0.24"
|
||||
|
||||
50
README.md
Normal file
50
README.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# Moomer
|
||||
|
||||
Zoomer application for macOS. A poor man's clone of [tsoding/boomer](https://github.com/tsoding/boomer).
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
$ cargo build --release
|
||||
$ cargo run --release
|
||||
```
|
||||
|
||||
## Controls
|
||||
|
||||
| Control | Description |
|
||||
|-------------------------------|---------------------------------------|
|
||||
| <kbd>q</kbd> | Quit the application. |
|
||||
| Drag with left mouse button | Move the image around. |
|
||||
| Scroll wheel or trackpad | Zoom in/out (centered on cursor). |
|
||||
|
||||
## Features
|
||||
|
||||
- Takes a screenshot of your primary display on startup
|
||||
- Fullscreen viewing without window decorations
|
||||
- Smooth zoom with trackpad gestures
|
||||
- Native macOS trackpad support
|
||||
- Pan by clicking and dragging
|
||||
|
||||
## Building
|
||||
|
||||
### Dependencies
|
||||
|
||||
```bash
|
||||
$ cargo build --release
|
||||
```
|
||||
|
||||
The following Rust crates are used:
|
||||
|
||||
- `screenshots` - Screen capture
|
||||
- `winit` - Window management and input handling
|
||||
- `pixels` - Pixel buffer rendering
|
||||
- `image` - Image processing
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
$ cargo run --release
|
||||
```
|
||||
|
||||
The application will immediately capture your screen and display it in fullscreen mode. Use your trackpad or mouse to zoom and pan around the screenshot.
|
||||
|
||||
202
src/main.rs
Normal file
202
src/main.rs
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
use pixels::{Pixels, SurfaceTexture};
|
||||
use screenshots::Screen;
|
||||
use winit::{
|
||||
event::{Event, WindowEvent, MouseScrollDelta, TouchPhase, ElementState, VirtualKeyCode},
|
||||
event_loop::{ControlFlow, EventLoop},
|
||||
window::WindowBuilder,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use winit::platform::macos::WindowExtMacOS;
|
||||
|
||||
struct ViewState {
|
||||
zoom: f32,
|
||||
offset_x: f32,
|
||||
offset_y: f32,
|
||||
drag_start: Option<(f32, f32)>,
|
||||
last_cursor_pos: (f32, f32),
|
||||
}
|
||||
|
||||
impl ViewState {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
zoom: 1.0,
|
||||
offset_x: 0.0,
|
||||
offset_y: 0.0,
|
||||
drag_start: None,
|
||||
last_cursor_pos: (0.0, 0.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let screens = Screen::all()?;
|
||||
let screen = screens.first().ok_or("No screens found")?;
|
||||
|
||||
let screenshot = screen.capture()?;
|
||||
let screenshot_width = screenshot.width();
|
||||
let screenshot_height = screenshot.height();
|
||||
|
||||
let img_buffer = image::RgbaImage::from_raw(
|
||||
screenshot_width,
|
||||
screenshot_height,
|
||||
screenshot.to_vec(),
|
||||
)
|
||||
.ok_or("Failed to create image buffer")?;
|
||||
|
||||
let event_loop = EventLoop::new();
|
||||
|
||||
let window = WindowBuilder::new()
|
||||
.with_title("Screenshot Viewer")
|
||||
.with_decorations(false)
|
||||
.build(&event_loop)?;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
window.set_simple_fullscreen(true);
|
||||
|
||||
let window_size = window.inner_size();
|
||||
let surface_texture = SurfaceTexture::new(window_size.width, window_size.height, &window);
|
||||
let mut pixels = Pixels::new(window_size.width, window_size.height, surface_texture)?;
|
||||
|
||||
let mut view_state = ViewState::new();
|
||||
|
||||
event_loop.run(move |event, _, control_flow| {
|
||||
*control_flow = ControlFlow::Wait;
|
||||
|
||||
match event {
|
||||
Event::WindowEvent { event, .. } => match event {
|
||||
WindowEvent::CloseRequested => {
|
||||
*control_flow = ControlFlow::Exit;
|
||||
}
|
||||
WindowEvent::Resized(new_size) => {
|
||||
if new_size.width > 0 && new_size.height > 0 {
|
||||
let _ = pixels.resize_surface(new_size.width, new_size.height);
|
||||
let _ = pixels.resize_buffer(new_size.width, new_size.height);
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
WindowEvent::CursorMoved { position, .. } => {
|
||||
view_state.last_cursor_pos = (position.x as f32, position.y as f32);
|
||||
|
||||
if let Some((start_x, start_y)) = view_state.drag_start {
|
||||
let dx = position.x as f32 - start_x;
|
||||
let dy = position.y as f32 - start_y;
|
||||
view_state.offset_x += dx;
|
||||
view_state.offset_y += dy;
|
||||
view_state.drag_start = Some((position.x as f32, position.y as f32));
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
WindowEvent::MouseInput { state, button, .. } => {
|
||||
match state {
|
||||
winit::event::ElementState::Pressed => {
|
||||
if button == winit::event::MouseButton::Left {
|
||||
view_state.drag_start = Some(view_state.last_cursor_pos);
|
||||
}
|
||||
}
|
||||
winit::event::ElementState::Released => {
|
||||
view_state.drag_start = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowEvent::MouseWheel { delta, phase, .. } => {
|
||||
if phase == TouchPhase::Moved || phase == TouchPhase::Started {
|
||||
match delta {
|
||||
MouseScrollDelta::LineDelta(_, y) => {
|
||||
let zoom_factor = 1.0 + (y * 0.1);
|
||||
view_state.zoom *= zoom_factor;
|
||||
view_state.zoom = view_state.zoom.max(0.1).min(10.0);
|
||||
|
||||
let cursor_x = view_state.last_cursor_pos.0;
|
||||
let cursor_y = view_state.last_cursor_pos.1;
|
||||
view_state.offset_x = cursor_x - (cursor_x - view_state.offset_x) * zoom_factor;
|
||||
view_state.offset_y = cursor_y - (cursor_y - view_state.offset_y) * zoom_factor;
|
||||
|
||||
window.request_redraw();
|
||||
}
|
||||
MouseScrollDelta::PixelDelta(pos) => {
|
||||
let zoom_delta = pos.y as f32;
|
||||
if zoom_delta.abs() > 0.1 {
|
||||
let zoom_factor = 1.0 + (zoom_delta * 0.005);
|
||||
view_state.zoom *= zoom_factor;
|
||||
view_state.zoom = view_state.zoom.max(0.1).min(10.0);
|
||||
|
||||
let cursor_x = view_state.last_cursor_pos.0;
|
||||
let cursor_y = view_state.last_cursor_pos.1;
|
||||
view_state.offset_x = cursor_x - (cursor_x - view_state.offset_x) * zoom_factor;
|
||||
view_state.offset_y = cursor_y - (cursor_y - view_state.offset_y) * zoom_factor;
|
||||
}
|
||||
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowEvent::KeyboardInput { input, .. } => {
|
||||
if input.state == ElementState::Pressed {
|
||||
if let Some(VirtualKeyCode::Q) = input.virtual_keycode {
|
||||
*control_flow = ControlFlow::Exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Event::RedrawRequested(_) => {
|
||||
render(
|
||||
pixels.frame_mut(),
|
||||
&img_buffer,
|
||||
&view_state,
|
||||
window_size.width,
|
||||
window_size.height,
|
||||
);
|
||||
if pixels.render().is_err() {
|
||||
*control_flow = ControlFlow::Exit;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn render(
|
||||
frame: &mut [u8],
|
||||
screenshot: &image::RgbaImage,
|
||||
view_state: &ViewState,
|
||||
window_width: u32,
|
||||
window_height: u32,
|
||||
) {
|
||||
for pixel in frame.chunks_exact_mut(4) {
|
||||
pixel[0] = 0;
|
||||
pixel[1] = 0;
|
||||
pixel[2] = 0;
|
||||
pixel[3] = 255;
|
||||
}
|
||||
|
||||
let screenshot_width = screenshot.width();
|
||||
let screenshot_height = screenshot.height();
|
||||
|
||||
for y in 0..window_height {
|
||||
for x in 0..window_width {
|
||||
let src_x_f = (x as f32 - view_state.offset_x) / view_state.zoom;
|
||||
let src_y_f = (y as f32 - view_state.offset_y) / view_state.zoom;
|
||||
|
||||
if src_x_f >= 0.0 && src_y_f >= 0.0 {
|
||||
let src_x = src_x_f as u32;
|
||||
let src_y = src_y_f as u32;
|
||||
|
||||
if src_x < screenshot_width && src_y < screenshot_height {
|
||||
let pixel = screenshot.get_pixel(src_x, src_y);
|
||||
let offset = (y * window_width + x) as usize * 4;
|
||||
|
||||
if offset + 3 < frame.len() {
|
||||
frame[offset] = pixel[0];
|
||||
frame[offset + 1] = pixel[1];
|
||||
frame[offset + 2] = pixel[2];
|
||||
frame[offset + 3] = pixel[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
target/.rustc_info.json
Normal file
1
target/.rustc_info.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"rustc_fingerprint":3429588090396759731,"outputs":{"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.91.0 (f8297e351 2025-10-28) (Homebrew)\nbinary: rustc\ncommit-hash: f8297e351a40c1439a467bbbb6879088047f50b3\ncommit-date: 2025-10-28\nhost: aarch64-apple-darwin\nrelease: 1.91.0\nLLVM version: 21.1.4\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n/opt/homebrew/Cellar/rust/1.91.0\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"aarch64\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"unix\"\ntarget_feature=\"aes\"\ntarget_feature=\"crc\"\ntarget_feature=\"dit\"\ntarget_feature=\"dotprod\"\ntarget_feature=\"dpb\"\ntarget_feature=\"dpb2\"\ntarget_feature=\"fcma\"\ntarget_feature=\"fhm\"\ntarget_feature=\"flagm\"\ntarget_feature=\"fp16\"\ntarget_feature=\"frintts\"\ntarget_feature=\"jsconv\"\ntarget_feature=\"lor\"\ntarget_feature=\"lse\"\ntarget_feature=\"neon\"\ntarget_feature=\"paca\"\ntarget_feature=\"pacg\"\ntarget_feature=\"pan\"\ntarget_feature=\"pmuv3\"\ntarget_feature=\"ras\"\ntarget_feature=\"rcpc\"\ntarget_feature=\"rcpc2\"\ntarget_feature=\"rdm\"\ntarget_feature=\"sb\"\ntarget_feature=\"sha2\"\ntarget_feature=\"sha3\"\ntarget_feature=\"ssbs\"\ntarget_feature=\"vh\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"macos\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"apple\"\nunix\n","stderr":""}},"successes":{}}
|
||||
3
target/CACHEDIR.TAG
Normal file
3
target/CACHEDIR.TAG
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
Signature: 8a477f597d28d172789f06886806bc55
|
||||
# This file is a cache directory tag created by cargo.
|
||||
# For information about cache directory tags see https://bford.info/cachedir/
|
||||
Loading…
Add table
Add a link
Reference in a new issue