added export functionality

This commit is contained in:
grimsace
2026-03-06 11:42:20 -06:00
parent 185710ae86
commit f14a8f653b
5 changed files with 843 additions and 0 deletions
+3
View File
@@ -8,3 +8,6 @@ eframe = "0.31"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
dirs = "6" dirs = "6"
rfd = "0.15"
tiny-skia = "0.11"
image = { version = "0.25", default-features = true }
+4
View File
@@ -35,6 +35,10 @@ A Rust desktop app for generating simple tabletop dungeon layouts.
- Generate control: - Generate control:
- Master seed input (`u64`) - Master seed input (`u64`)
- `Random` seed button - `Random` seed button
- `Reset` button
- `Export Image` button
- Export format selector: `.png`, `.jpeg`, `.webp`, `.svg`
- Export uses an OS folder picker to choose save location
## Generation Behavior ## Generation Behavior
+770
View File
@@ -0,0 +1,770 @@
use std::collections::HashSet;
use std::fmt::Write as _;
use std::path::PathBuf;
use image::{DynamicImage, ImageFormat, RgbaImage};
use rfd::FileDialog;
use tiny_skia::{FillRule, Paint, PathBuilder, Pixmap, Rect, Stroke, StrokeDash, Transform};
use crate::layout::{Door, DungeonLayout, Room};
use crate::ui::{ExportFormat, UiSettings};
const BG: (u8, u8, u8, u8) = (24, 24, 26, 255);
const GRID: (u8, u8, u8, u8) = (130, 130, 130, 255);
const ROOM: (u8, u8, u8, u8) = (70, 120, 160, 255);
const CORRIDOR: (u8, u8, u8, u8) = (210, 190, 120, 255);
const BLACK: (u8, u8, u8, u8) = (0, 0, 0, 255);
const OPEN_DOOR: (u8, u8, u8, u8) = (220, 70, 70, 255);
const LOCKED_DOOR: (u8, u8, u8, u8) = (80, 200, 120, 255);
const ARCHWAY: (u8, u8, u8, u8) = (70, 130, 220, 255);
#[derive(Debug, Clone, Copy)]
enum DoorStyle {
Solid,
LongDash,
ShortDash,
Dotted,
}
#[derive(Debug, Clone, Copy)]
struct ExportGeometry {
cols: usize,
rows: usize,
cell: f32,
pad: f32,
width: u32,
height: u32,
}
impl ExportGeometry {
fn new(cols: usize, rows: usize) -> Self {
let cell = 64.0;
let pad = 40.0;
let width = (pad * 2.0 + cols as f32 * cell).round() as u32;
let height = (pad * 2.0 + rows as f32 * cell).round() as u32;
Self {
cols,
rows,
cell,
pad,
width,
height,
}
}
fn left(&self) -> f32 {
self.pad
}
fn top(&self) -> f32 {
self.pad
}
fn cell_rect(&self, col: usize, row: usize) -> Option<Rect> {
Rect::from_xywh(
self.left() + col as f32 * self.cell,
self.top() + row as f32 * self.cell,
self.cell,
self.cell,
)
}
fn cell_center(&self, col: usize, row: usize) -> (f32, f32) {
(
self.left() + (col as f32 + 0.5) * self.cell,
self.top() + (row as f32 + 0.5) * self.cell,
)
}
}
pub fn export_with_dialog(
layout: &DungeonLayout,
settings: &UiSettings,
) -> Result<PathBuf, String> {
let ext = settings.export_format.extension();
let default_name = format!("dungeon_export.{ext}");
let mut dialog = FileDialog::new();
dialog = dialog
.set_title("Export image")
.set_file_name(&default_name)
.add_filter(settings.export_format.label(), &[ext]);
let mut path = dialog
.save_file()
.ok_or_else(|| "Export canceled".to_string())?;
if path.extension().is_none() {
path.set_extension(ext);
}
match settings.export_format {
ExportFormat::Svg => {
let svg = build_svg(layout, settings);
std::fs::write(&path, svg).map_err(|e| format!("Failed writing SVG: {e}"))?;
}
ExportFormat::Png | ExportFormat::Jpeg | ExportFormat::Webp => {
let pixmap = render_pixmap(layout, settings)?;
let rgba = RgbaImage::from_raw(pixmap.width(), pixmap.height(), pixmap.data().to_vec())
.ok_or_else(|| "Failed to build image buffer".to_string())?;
let img = DynamicImage::ImageRgba8(rgba);
let format = match settings.export_format {
ExportFormat::Png => ImageFormat::Png,
ExportFormat::Jpeg => ImageFormat::Jpeg,
ExportFormat::Webp => ImageFormat::WebP,
ExportFormat::Svg => unreachable!(),
};
img.save_with_format(&path, format)
.map_err(|e| format!("Failed writing image: {e}"))?;
}
}
Ok(path)
}
fn render_pixmap(layout: &DungeonLayout, settings: &UiSettings) -> Result<Pixmap, String> {
let g = ExportGeometry::new(settings.cols, settings.rows);
let mut pixmap =
Pixmap::new(g.width, g.height).ok_or_else(|| "Failed to allocate canvas".to_string())?;
fill_bg(&mut pixmap, BG);
let wall_w = (g.cell / 5.0).max(1.0);
let door_w = (g.cell / 10.0).max(1.0);
draw_grid(&mut pixmap, &g);
let mut corridor_cells = HashSet::new();
let mut corridor_edges = HashSet::new();
let mut room_cells = HashSet::new();
let mut door_edges = HashSet::new();
for door in &layout.doors {
door_edges.insert(norm_edge(door.from, door.to));
}
for corridor in &layout.corridors {
for &cell in &corridor.path {
corridor_cells.insert(cell);
}
for pair in corridor.path.windows(2) {
corridor_edges.insert(norm_edge(pair[0], pair[1]));
}
}
for room in &layout.rooms {
for x in room.x..(room.x + room.width) {
for y in room.y..(room.y + room.height) {
room_cells.insert((x, y));
}
}
}
for &(x, y) in &corridor_cells {
fill_rect_cell(&mut pixmap, &g, x, y, CORRIDOR);
if settings.colorblind_mode {
let (cx, cy) = g.cell_center(x, y);
draw_dot(&mut pixmap, cx, cy, g.cell * 0.14, BLACK);
}
}
for room in &layout.rooms {
fill_rect_room(&mut pixmap, &g, room, ROOM);
if settings.colorblind_mode {
draw_room_crosshatch(&mut pixmap, &g, room, 1.5, BLACK);
}
}
draw_cell_walls(
&mut pixmap,
&g,
&corridor_cells,
Some(&corridor_edges),
&door_edges,
wall_w,
BLACK,
);
draw_cell_walls(
&mut pixmap,
&g,
&room_cells,
None,
&door_edges,
wall_w,
BLACK,
);
for door in &layout.doors {
let (color, style) = if settings.colorblind_mode {
let s = if door.archway {
DoorStyle::Dotted
} else if door.locked {
DoorStyle::ShortDash
} else {
DoorStyle::LongDash
};
(BLACK, s)
} else if door.archway {
(ARCHWAY, DoorStyle::Solid)
} else if door.locked {
(LOCKED_DOOR, DoorStyle::Solid)
} else {
(OPEN_DOOR, DoorStyle::Solid)
};
draw_door(&mut pixmap, &g, door, door_w, color, style);
}
Ok(pixmap)
}
fn build_svg(layout: &DungeonLayout, settings: &UiSettings) -> String {
let g = ExportGeometry::new(settings.cols, settings.rows);
let wall_w = (g.cell / 5.0).max(1.0);
let door_w = (g.cell / 10.0).max(1.0);
let mut s = String::new();
let _ = writeln!(
s,
"<svg xmlns='http://www.w3.org/2000/svg' width='{}' height='{}' viewBox='0 0 {} {}'>",
g.width, g.height, g.width, g.height
);
let _ = writeln!(
s,
"<rect width='100%' height='100%' fill='rgb({},{},{})'/>",
BG.0, BG.1, BG.2
);
for c in 0..=g.cols {
let x = g.left() + c as f32 * g.cell;
let _ = writeln!(
s,
"<line x1='{x}' y1='{}' x2='{x}' y2='{}' stroke='rgb({},{},{})' stroke-width='1'/>",
g.top(),
g.top() + g.rows as f32 * g.cell,
GRID.0,
GRID.1,
GRID.2
);
}
for r in 0..=g.rows {
let y = g.top() + r as f32 * g.cell;
let _ = writeln!(
s,
"<line x1='{}' y1='{y}' x2='{}' y2='{y}' stroke='rgb({},{},{})' stroke-width='1'/>",
g.left(),
g.left() + g.cols as f32 * g.cell,
GRID.0,
GRID.1,
GRID.2
);
}
let mut corridor_cells = HashSet::new();
let mut corridor_edges = HashSet::new();
let mut room_cells = HashSet::new();
let mut door_edges = HashSet::new();
for door in &layout.doors {
door_edges.insert(norm_edge(door.from, door.to));
}
for corridor in &layout.corridors {
for &cell in &corridor.path {
corridor_cells.insert(cell);
}
for pair in corridor.path.windows(2) {
corridor_edges.insert(norm_edge(pair[0], pair[1]));
}
}
for room in &layout.rooms {
for x in room.x..(room.x + room.width) {
for y in room.y..(room.y + room.height) {
room_cells.insert((x, y));
}
}
}
for &(x, y) in &corridor_cells {
let px = g.left() + x as f32 * g.cell;
let py = g.top() + y as f32 * g.cell;
let _ = writeln!(
s,
"<rect x='{px}' y='{py}' width='{}' height='{}' fill='rgb({},{},{})'/>",
g.cell, g.cell, CORRIDOR.0, CORRIDOR.1, CORRIDOR.2
);
if settings.colorblind_mode {
let (cx, cy) = g.cell_center(x, y);
let _ = writeln!(
s,
"<circle cx='{cx}' cy='{cy}' r='{}' fill='black'/>",
g.cell * 0.14
);
}
}
for room in &layout.rooms {
let px = g.left() + room.x as f32 * g.cell;
let py = g.top() + room.y as f32 * g.cell;
let _ = writeln!(
s,
"<rect x='{px}' y='{py}' width='{}' height='{}' fill='rgb({},{},{})'/>",
room.width as f32 * g.cell,
room.height as f32 * g.cell,
ROOM.0,
ROOM.1,
ROOM.2
);
if settings.colorblind_mode {
for x in room.x..(room.x + room.width) {
for y in room.y..(room.y + room.height) {
let rx = g.left() + x as f32 * g.cell + 2.0;
let ry = g.top() + y as f32 * g.cell + 2.0;
let rr = g.cell - 4.0;
let _ = writeln!(
s,
"<line x1='{rx}' y1='{ry}' x2='{}' y2='{}' stroke='black' stroke-width='1.5'/>",
rx + rr,
ry + rr
);
let _ = writeln!(
s,
"<line x1='{}' y1='{ry}' x2='{rx}' y2='{}' stroke='black' stroke-width='1.5'/>",
rx + rr,
ry + rr
);
}
}
}
}
append_svg_walls(
&mut s,
&g,
&corridor_cells,
Some(&corridor_edges),
&door_edges,
wall_w,
);
append_svg_walls(&mut s, &g, &room_cells, None, &door_edges, wall_w);
for door in &layout.doors {
let (color, dash) = if settings.colorblind_mode {
if door.archway {
("black", Some("2,4"))
} else if door.locked {
("black", Some("6,4"))
} else {
("black", Some("14,7"))
}
} else if door.archway {
("rgb(70,130,220)", None)
} else if door.locked {
("rgb(80,200,120)", None)
} else {
("rgb(220,70,70)", None)
};
if let Some((x1, y1, x2, y2)) = door_line_points(&g, door.from, door.to) {
if let Some(pattern) = dash {
let _ = writeln!(
s,
"<line x1='{x1}' y1='{y1}' x2='{x2}' y2='{y2}' stroke='{color}' stroke-width='{door_w}' stroke-dasharray='{pattern}'/>"
);
} else {
let _ = writeln!(
s,
"<line x1='{x1}' y1='{y1}' x2='{x2}' y2='{y2}' stroke='{color}' stroke-width='{door_w}'/>"
);
}
}
}
s.push_str("</svg>\n");
s
}
fn fill_bg(pixmap: &mut Pixmap, color: (u8, u8, u8, u8)) {
let mut paint = Paint::default();
paint.set_color_rgba8(color.0, color.1, color.2, color.3);
let Some(rect) = Rect::from_xywh(0.0, 0.0, pixmap.width() as f32, pixmap.height() as f32)
else {
return;
};
pixmap.fill_rect(rect, &paint, Transform::identity(), None);
}
fn draw_grid(pixmap: &mut Pixmap, g: &ExportGeometry) {
for c in 0..=g.cols {
let x = g.left() + c as f32 * g.cell;
draw_line(
pixmap,
x,
g.top(),
x,
g.top() + g.rows as f32 * g.cell,
1.0,
GRID,
DoorStyle::Solid,
);
}
for r in 0..=g.rows {
let y = g.top() + r as f32 * g.cell;
draw_line(
pixmap,
g.left(),
y,
g.left() + g.cols as f32 * g.cell,
y,
1.0,
GRID,
DoorStyle::Solid,
);
}
}
fn fill_rect_cell(
pixmap: &mut Pixmap,
g: &ExportGeometry,
col: usize,
row: usize,
color: (u8, u8, u8, u8),
) {
let Some(rect) = g.cell_rect(col, row) else {
return;
};
let mut paint = Paint::default();
paint.set_color_rgba8(color.0, color.1, color.2, color.3);
pixmap.fill_rect(rect, &paint, Transform::identity(), None);
}
fn fill_rect_room(pixmap: &mut Pixmap, g: &ExportGeometry, room: &Room, color: (u8, u8, u8, u8)) {
let Some(rect) = Rect::from_xywh(
g.left() + room.x as f32 * g.cell,
g.top() + room.y as f32 * g.cell,
room.width as f32 * g.cell,
room.height as f32 * g.cell,
) else {
return;
};
let mut paint = Paint::default();
paint.set_color_rgba8(color.0, color.1, color.2, color.3);
pixmap.fill_rect(rect, &paint, Transform::identity(), None);
}
fn draw_dot(pixmap: &mut Pixmap, cx: f32, cy: f32, radius: f32, color: (u8, u8, u8, u8)) {
let mut pb = PathBuilder::new();
pb.push_circle(cx, cy, radius);
let Some(path) = pb.finish() else {
return;
};
let mut paint = Paint::default();
paint.set_color_rgba8(color.0, color.1, color.2, color.3);
pixmap.fill_path(
&path,
&paint,
FillRule::Winding,
Transform::identity(),
None,
);
}
fn draw_room_crosshatch(
pixmap: &mut Pixmap,
g: &ExportGeometry,
room: &Room,
width: f32,
color: (u8, u8, u8, u8),
) {
for x in room.x..(room.x + room.width) {
for y in room.y..(room.y + room.height) {
if let Some(rect) = g.cell_rect(x, y) {
let pad = 2.0;
draw_line(
pixmap,
rect.left() + pad,
rect.top() + pad,
rect.right() - pad,
rect.bottom() - pad,
width,
color,
DoorStyle::Solid,
);
draw_line(
pixmap,
rect.right() - pad,
rect.top() + pad,
rect.left() + pad,
rect.bottom() - pad,
width,
color,
DoorStyle::Solid,
);
}
}
}
}
fn draw_cell_walls(
pixmap: &mut Pixmap,
g: &ExportGeometry,
cells: &HashSet<(usize, usize)>,
connected_edges: Option<&HashSet<((usize, usize), (usize, usize))>>,
door_edges: &HashSet<((usize, usize), (usize, usize))>,
width: f32,
color: (u8, u8, u8, u8),
) {
for &(col, row) in cells {
if let Some(rect) = g.cell_rect(col, row) {
let right = (col + 1, row);
let bottom = (col, row + 1);
let should_left = col == 0
|| (!cells.contains(&(col - 1, row))
&& !door_edges.contains(&norm_edge((col, row), (col - 1, row))));
let right_connected = connected_edges
.map(|set| set.contains(&norm_edge((col, row), right)))
.unwrap_or(false);
let should_right = (!cells.contains(&right) || !right_connected)
&& !door_edges.contains(&norm_edge((col, row), right));
let should_top = row == 0
|| (!cells.contains(&(col, row - 1))
&& !door_edges.contains(&norm_edge((col, row), (col, row - 1))));
let bottom_connected = connected_edges
.map(|set| set.contains(&norm_edge((col, row), bottom)))
.unwrap_or(false);
let should_bottom = (!cells.contains(&bottom) || !bottom_connected)
&& !door_edges.contains(&norm_edge((col, row), bottom));
if should_left {
draw_line(
pixmap,
rect.left(),
rect.top(),
rect.left(),
rect.bottom(),
width,
color,
DoorStyle::Solid,
);
}
if should_right {
draw_line(
pixmap,
rect.right(),
rect.top(),
rect.right(),
rect.bottom(),
width,
color,
DoorStyle::Solid,
);
}
if should_top {
draw_line(
pixmap,
rect.left(),
rect.top(),
rect.right(),
rect.top(),
width,
color,
DoorStyle::Solid,
);
}
if should_bottom {
draw_line(
pixmap,
rect.left(),
rect.bottom(),
rect.right(),
rect.bottom(),
width,
color,
DoorStyle::Solid,
);
}
}
}
}
fn append_svg_walls(
out: &mut String,
g: &ExportGeometry,
cells: &HashSet<(usize, usize)>,
connected_edges: Option<&HashSet<((usize, usize), (usize, usize))>>,
door_edges: &HashSet<((usize, usize), (usize, usize))>,
width: f32,
) {
for &(col, row) in cells {
let left = g.left() + col as f32 * g.cell;
let top = g.top() + row as f32 * g.cell;
let right = left + g.cell;
let bottom = top + g.cell;
let right_cell = (col + 1, row);
let bottom_cell = (col, row + 1);
let left_edge = col == 0
|| (!cells.contains(&(col - 1, row))
&& !door_edges.contains(&norm_edge((col, row), (col - 1, row))));
let right_connected = connected_edges
.map(|set| set.contains(&norm_edge((col, row), right_cell)))
.unwrap_or(false);
let right_edge = (!cells.contains(&right_cell) || !right_connected)
&& !door_edges.contains(&norm_edge((col, row), right_cell));
let top_edge = row == 0
|| (!cells.contains(&(col, row - 1))
&& !door_edges.contains(&norm_edge((col, row), (col, row - 1))));
let bottom_connected = connected_edges
.map(|set| set.contains(&norm_edge((col, row), bottom_cell)))
.unwrap_or(false);
let bottom_edge = (!cells.contains(&bottom_cell) || !bottom_connected)
&& !door_edges.contains(&norm_edge((col, row), bottom_cell));
if left_edge {
let _ = writeln!(
out,
"<line x1='{left}' y1='{top}' x2='{left}' y2='{bottom}' stroke='black' stroke-width='{width}'/>"
);
}
if right_edge {
let _ = writeln!(
out,
"<line x1='{right}' y1='{top}' x2='{right}' y2='{bottom}' stroke='black' stroke-width='{width}'/>"
);
}
if top_edge {
let _ = writeln!(
out,
"<line x1='{left}' y1='{top}' x2='{right}' y2='{top}' stroke='black' stroke-width='{width}'/>"
);
}
if bottom_edge {
let _ = writeln!(
out,
"<line x1='{left}' y1='{bottom}' x2='{right}' y2='{bottom}' stroke='black' stroke-width='{width}'/>"
);
}
}
}
fn draw_door(
pixmap: &mut Pixmap,
g: &ExportGeometry,
door: &Door,
width: f32,
color: (u8, u8, u8, u8),
style: DoorStyle,
) {
let Some((x1, y1, x2, y2)) = door_line_points(g, door.from, door.to) else {
return;
};
draw_line(pixmap, x1, y1, x2, y2, width, color, style);
}
fn door_line_points(
g: &ExportGeometry,
a: (usize, usize),
b: (usize, usize),
) -> Option<(f32, f32, f32, f32)> {
if a.0.abs_diff(b.0) + a.1.abs_diff(b.1) != 1 {
return None;
}
if a.0 != b.0 {
let x = g.left() + (a.0.max(b.0) as f32) * g.cell;
let row = a.1;
let y0 = g.top() + row as f32 * g.cell;
let y1 = y0 + g.cell;
Some((x, y0, x, y1))
} else {
let y = g.top() + (a.1.max(b.1) as f32) * g.cell;
let col = a.0;
let x0 = g.left() + col as f32 * g.cell;
let x1 = x0 + g.cell;
Some((x0, y, x1, y))
}
}
fn draw_line(
pixmap: &mut Pixmap,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
width: f32,
color: (u8, u8, u8, u8),
style: DoorStyle,
) {
match style {
DoorStyle::Solid => stroke_path(pixmap, x1, y1, x2, y2, width, color, None),
DoorStyle::LongDash => stroke_path(pixmap, x1, y1, x2, y2, width, color, Some((14.0, 7.0))),
DoorStyle::ShortDash => stroke_path(pixmap, x1, y1, x2, y2, width, color, Some((6.0, 4.0))),
DoorStyle::Dotted => dotted_line(pixmap, x1, y1, x2, y2, width, color),
}
}
fn stroke_path(
pixmap: &mut Pixmap,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
width: f32,
color: (u8, u8, u8, u8),
dash: Option<(f32, f32)>,
) {
let mut pb = PathBuilder::new();
pb.move_to(x1, y1);
pb.line_to(x2, y2);
let Some(path) = pb.finish() else {
return;
};
let mut paint = Paint::default();
paint.set_color_rgba8(color.0, color.1, color.2, color.3);
let mut stroke = Stroke::default();
stroke.width = width;
if let Some((dash_len, gap_len)) = dash {
stroke.dash = StrokeDash::new(vec![dash_len, gap_len], 0.0);
}
pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
}
fn dotted_line(
pixmap: &mut Pixmap,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
width: f32,
color: (u8, u8, u8, u8),
) {
let dx = x2 - x1;
let dy = y2 - y1;
let len = (dx * dx + dy * dy).sqrt();
if len <= 0.0 {
return;
}
let ux = dx / len;
let uy = dy / len;
let mut d = 0.0_f32;
let step = 6.0;
let radius = (width * 0.35).max(1.0);
while d <= len {
draw_dot(pixmap, x1 + ux * d, y1 + uy * d, radius, color);
d += step;
}
}
fn norm_edge(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
if a <= b { (a, b) } else { (b, a) }
}
+6
View File
@@ -1,3 +1,4 @@
mod exporter;
mod layout; mod layout;
mod seed; mod seed;
mod settings; mod settings;
@@ -68,6 +69,11 @@ impl eframe::App for DungeonApp {
if panel_result.reset_clicked || panel_result.settings_changed { if panel_result.reset_clicked || panel_result.settings_changed {
self.regenerate_layout(); self.regenerate_layout();
} }
if panel_result.export_clicked {
if let Err(err) = exporter::export_with_dialog(&self.layout, &self.settings) {
eprintln!("{err}");
}
}
egui::SidePanel::right("legend_panel") egui::SidePanel::right("legend_panel")
.resizable(false) .resizable(false)
+60
View File
@@ -15,6 +15,40 @@ impl Default for Tab {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ExportFormat {
Png,
Jpeg,
Webp,
Svg,
}
impl Default for ExportFormat {
fn default() -> Self {
Self::Png
}
}
impl ExportFormat {
pub fn extension(self) -> &'static str {
match self {
ExportFormat::Png => "png",
ExportFormat::Jpeg => "jpeg",
ExportFormat::Webp => "webp",
ExportFormat::Svg => "svg",
}
}
pub fn label(self) -> &'static str {
match self {
ExportFormat::Png => ".png",
ExportFormat::Jpeg => ".jpeg",
ExportFormat::Webp => ".webp",
ExportFormat::Svg => ".svg",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)] #[serde(default)]
pub struct UiSettings { pub struct UiSettings {
@@ -32,6 +66,7 @@ pub struct UiSettings {
pub locked_door_percent: usize, pub locked_door_percent: usize,
pub allow_middle_corridor_doors: bool, pub allow_middle_corridor_doors: bool,
pub colorblind_mode: bool, pub colorblind_mode: bool,
pub export_format: ExportFormat,
active_tab: Tab, active_tab: Tab,
} }
@@ -52,6 +87,7 @@ impl Default for UiSettings {
locked_door_percent: 25, locked_door_percent: 25,
allow_middle_corridor_doors: false, allow_middle_corridor_doors: false,
colorblind_mode: false, colorblind_mode: false,
export_format: ExportFormat::Png,
active_tab: Tab::Generate, active_tab: Tab::Generate,
} }
} }
@@ -61,6 +97,7 @@ impl Default for UiSettings {
pub struct SidePanelResult { pub struct SidePanelResult {
pub settings_changed: bool, pub settings_changed: bool,
pub reset_clicked: bool, pub reset_clicked: bool,
pub export_clicked: bool,
} }
pub fn draw_side_panel(ctx: &egui::Context, settings: &mut UiSettings) -> SidePanelResult { pub fn draw_side_panel(ctx: &egui::Context, settings: &mut UiSettings) -> SidePanelResult {
@@ -148,6 +185,29 @@ fn draw_generate_tab(ui: &mut egui::Ui, settings: &mut UiSettings, result: &mut
if ui.button("Reset").clicked() { if ui.button("Reset").clicked() {
result.reset_clicked = true; result.reset_clicked = true;
} }
ui.add_space(12.0);
ui.separator();
ui.add_space(8.0);
ui.label(RichText::new("Export").strong());
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.label("Format");
egui::ComboBox::from_id_salt("export_format")
.selected_text(settings.export_format.label())
.show_ui(ui, |ui| {
ui.selectable_value(&mut settings.export_format, ExportFormat::Png, ".png");
ui.selectable_value(&mut settings.export_format, ExportFormat::Jpeg, ".jpeg");
ui.selectable_value(&mut settings.export_format, ExportFormat::Webp, ".webp");
ui.selectable_value(&mut settings.export_format, ExportFormat::Svg, ".svg");
});
});
ui.add_space(6.0);
if ui.button("Export Image").clicked() {
result.export_clicked = true;
}
} }
fn draw_layout_tab(ui: &mut egui::Ui, settings: &mut UiSettings, result: &mut SidePanelResult) { fn draw_layout_tab(ui: &mut egui::Ui, settings: &mut UiSettings, result: &mut SidePanelResult) {