added copy paste for the images

This commit is contained in:
grimsace
2026-04-21 14:30:42 -05:00
parent 82f8323ff4
commit 0534bddaef
3 changed files with 124 additions and 12 deletions
Generated
+1
View File
@@ -118,6 +118,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
name = "advanced_erosion_terrain_heightmap_generation" name = "advanced_erosion_terrain_heightmap_generation"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"arboard",
"eframe", "eframe",
"egui", "egui",
"image", "image",
+6
View File
@@ -4,8 +4,14 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
arboard = "3.6"
eframe = "0.33" eframe = "0.33"
egui = "0.33" egui = "0.33"
image = { version = "0.25", default-features = false, features = ["png"] } image = { version = "0.25", default-features = false, features = ["png"] }
rand = "0.9" rand = "0.9"
rayon = "1.10" rayon = "1.10"
[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
+117 -12
View File
@@ -1,9 +1,11 @@
use std::sync::mpsc::{self, Receiver, TryRecvError}; use std::sync::mpsc::{self, Receiver, TryRecvError};
use std::thread; use std::thread;
use arboard::{Clipboard, ImageData};
use eframe::egui::{ use eframe::egui::{
self, Color32, ColorImage, Context, DragValue, Frame, RichText, Sense, Slider, TextureHandle, self, Align2, Color32, ColorImage, Context, CornerRadius, DragValue, Event, Frame, Key,
TextureOptions, Ui, Vec2, Response, RichText, Sense, Slider, Stroke, StrokeKind, TextureHandle, TextureOptions, Ui,
Vec2,
}; };
use rand::Rng; use rand::Rng;
@@ -114,6 +116,56 @@ impl TerrainApp {
}); });
} }
fn copy_preview(&mut self, preview: PreviewFocus) {
let Some(generated) = &self.generated else {
self.status_message = String::from("No generated image is available to copy.");
return;
};
let image = match preview {
PreviewFocus::Heightmap => &generated.heightmap_image,
PreviewFocus::Bumpmap => &generated.bumpmap_image,
};
match copy_gray_image_to_clipboard(image) {
Ok(()) => {
self.status_message = match preview {
PreviewFocus::Heightmap => String::from("Heightmap copied to clipboard."),
PreviewFocus::Bumpmap => String::from("Bumpmap copied to clipboard."),
};
}
Err(error) => {
self.status_message = format!("Failed to copy image: {error}");
}
}
}
fn preview_context_menu(&mut self, response: &Response, preview: PreviewFocus) {
response.context_menu(|ui| {
if ui.button("Copy").clicked() {
self.copy_preview(preview);
ui.close();
}
});
}
fn overlay_copy_button(&mut self, ui: &mut Ui, anchor_rect: egui::Rect, preview: PreviewFocus) {
let button_size = Vec2::new(28.0, 28.0);
let button_rect = Align2::RIGHT_TOP
.align_size_within_rect(button_size, anchor_rect.shrink2(Vec2::new(8.0, 8.0)));
let response = ui.put(
button_rect,
egui::Button::new(RichText::new("🗍").size(14.0)),
);
if response.clicked() {
self.copy_preview(preview);
}
response.on_hover_text("Copy");
}
fn tab_button(ui: &mut Ui, current: &mut LeftTab, tab: LeftTab, label: &str) { fn tab_button(ui: &mut Ui, current: &mut LeftTab, tab: LeftTab, label: &str) {
let selected = *current == tab; let selected = *current == tab;
if ui.selectable_label(selected, label).clicked() { if ui.selectable_label(selected, label).clicked() {
@@ -231,24 +283,29 @@ impl TerrainApp {
}); });
return; return;
}; };
let heightmap_texture = textures.heightmap.clone();
let bumpmap_texture = textures.bumpmap.clone();
let available = ui.available_size(); let available = ui.available_size();
let bottom_strip_height = (available.y * 0.24).max(120.0).min(220.0); let bottom_strip_height = (available.y * 0.24).max(120.0).min(220.0);
let main_height = (available.y - bottom_strip_height - 8.0).max(100.0); let main_height = (available.y - bottom_strip_height - 8.0).max(100.0);
let main_texture = match self.preview_focus { let main_texture = match self.preview_focus {
PreviewFocus::Heightmap => &textures.heightmap, PreviewFocus::Heightmap => &heightmap_texture,
PreviewFocus::Bumpmap => &textures.bumpmap, PreviewFocus::Bumpmap => &bumpmap_texture,
}; };
render_clickable_image(ui, main_texture, Vec2::new(available.x, main_height), false); let main_response =
render_clickable_image(ui, main_texture, Vec2::new(available.x, main_height), true);
self.preview_context_menu(&main_response, self.preview_focus);
self.overlay_copy_button(ui, main_response.rect, self.preview_focus);
ui.add_space(8.0); ui.add_space(8.0);
let thumb_width = (available.x * 0.28).max(120.0).min(240.0); let thumb_width = (available.x * 0.28).max(120.0).min(240.0);
let panel_width = (thumb_width + 32.0).min(available.x); let panel_width = (thumb_width + 32.0).min(available.x);
let secondary_texture = match self.preview_focus { let secondary_texture = match self.preview_focus {
PreviewFocus::Heightmap => &textures.bumpmap, PreviewFocus::Heightmap => &bumpmap_texture,
PreviewFocus::Bumpmap => &textures.heightmap, PreviewFocus::Bumpmap => &heightmap_texture,
}; };
let secondary_focus = match self.preview_focus { let secondary_focus = match self.preview_focus {
PreviewFocus::Heightmap => PreviewFocus::Bumpmap, PreviewFocus::Heightmap => PreviewFocus::Bumpmap,
@@ -266,12 +323,14 @@ impl TerrainApp {
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.label(RichText::new(secondary_label).strong()); ui.label(RichText::new(secondary_label).strong());
ui.add_space(6.0); ui.add_space(6.0);
if render_clickable_image( let secondary_response = render_clickable_image(
ui, ui,
secondary_texture, secondary_texture,
Vec2::new(thumb_width, bottom_strip_height - 32.0), Vec2::new(thumb_width, bottom_strip_height - 32.0),
true, true,
) { );
self.preview_context_menu(&secondary_response, secondary_focus);
if secondary_response.clicked() {
self.preview_focus = secondary_focus; self.preview_focus = secondary_focus;
} }
}); });
@@ -283,6 +342,24 @@ impl TerrainApp {
impl eframe::App for TerrainApp { impl eframe::App for TerrainApp {
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) { fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
let wants_keyboard_input = ctx.wants_keyboard_input();
let should_copy = ctx.input(|input| {
!wants_keyboard_input
&& input.events.iter().any(|event| match event {
Event::Copy => true,
Event::Key {
key: Key::C,
pressed: true,
modifiers,
..
} => modifiers.ctrl || modifiers.command,
_ => false,
})
});
if should_copy {
self.copy_preview(self.preview_focus);
}
self.poll_generation(ctx); self.poll_generation(ctx);
self.ui_left_panel(ctx); self.ui_left_panel(ctx);
self.ui_right_panel(ctx); self.ui_right_panel(ctx);
@@ -335,7 +412,7 @@ fn render_clickable_image(
texture: &TextureHandle, texture: &TextureHandle,
max_size: Vec2, max_size: Vec2,
clickable: bool, clickable: bool,
) -> bool { ) -> Response {
let image_size = texture.size_vec2(); let image_size = texture.size_vec2();
let scale = (max_size.x / image_size.x).min(max_size.y / image_size.y); let scale = (max_size.x / image_size.x).min(max_size.y / image_size.y);
let desired_size = image_size * scale.max(0.01); let desired_size = image_size * scale.max(0.01);
@@ -344,7 +421,35 @@ fn render_clickable_image(
.sense(if clickable { .sense(if clickable {
Sense::click() Sense::click()
} else { } else {
Sense::hover() Sense::click()
}); });
ui.add(image).clicked() let response = ui.add(image);
if response.hovered() {
ui.painter().rect_stroke(
response.rect.expand(4.0),
CornerRadius::same(8),
Stroke::new(2.0, Color32::from_rgba_premultiplied(180, 220, 255, 180)),
StrokeKind::Outside,
);
}
response
}
fn copy_gray_image_to_clipboard(image: &image::GrayImage) -> Result<(), String> {
let mut rgba = Vec::with_capacity((image.width() * image.height() * 4) as usize);
for pixel in image.pixels() {
let gray = pixel.0[0];
rgba.extend_from_slice(&[gray, gray, gray, 255]);
}
let data = ImageData {
width: image.width() as usize,
height: image.height() as usize,
bytes: rgba.into(),
};
let mut clipboard = Clipboard::new().map_err(|error| error.to_string())?;
clipboard.set_image(data).map_err(|error| error.to_string())
} }