added a colorblind mode accessability feature

This commit is contained in:
grimsace
2026-03-06 11:16:17 -06:00
parent ad47b2915f
commit e88806412e
3 changed files with 343 additions and 18 deletions
+9
View File
@@ -23,6 +23,15 @@ A Rust desktop app for generating simple tabletop dungeon layouts.
- `Room/Hallway Door Chance (%)` - `Room/Hallway Door Chance (%)`
- `Locked Door Chance (%)` - `Locked Door Chance (%)`
- `Allow Middle Corridor Doors` toggle - `Allow Middle Corridor Doors` toggle
- Accessibility:
- `Colorblind Mode` toggle (right panel)
- Pattern/style encoding:
- Rooms: crosshatch
- Corridors: dotted fill
- Walls: solid lines
- Doors: long dashes
- Locked doors: short dashes
- Archways: dotted lines
- Generate control: - Generate control:
- Master seed input (`u64`) - Master seed input (`u64`)
- `Random` seed button - `Random` seed button
+332 -18
View File
@@ -75,14 +75,29 @@ impl eframe::App for DungeonApp {
.default_width(200.0) .default_width(200.0)
.show_separator_line(true) .show_separator_line(true)
.show(ctx, |ui| { .show(ctx, |ui| {
ui.heading("Accessibility");
ui.add_space(6.0);
ui.checkbox(&mut self.settings.colorblind_mode, "Colorblind Mode");
ui.add_space(10.0);
ui.separator();
ui.add_space(8.0);
ui.heading("Legend"); ui.heading("Legend");
ui.add_space(8.0); ui.add_space(8.0);
draw_legend_entry(ui, Color32::from_rgb(70, 120, 160), "Rooms"); if self.settings.colorblind_mode {
draw_legend_entry(ui, Color32::from_rgb(210, 190, 120), "Corridors"); draw_legend_entry_colorblind(ui, "Rooms", LegendStyle::Crosshatch);
draw_legend_entry(ui, Color32::BLACK, "Walls"); draw_legend_entry_colorblind(ui, "Corridors", LegendStyle::Dots);
draw_legend_entry(ui, Color32::from_rgb(220, 70, 70), "Doors"); draw_legend_entry_colorblind(ui, "Walls", LegendStyle::SolidLine);
draw_legend_entry(ui, Color32::from_rgb(80, 200, 120), "Locked Doors"); draw_legend_entry_colorblind(ui, "Doors", LegendStyle::LongDash);
draw_legend_entry(ui, Color32::from_rgb(70, 130, 220), "Archways"); draw_legend_entry_colorblind(ui, "Locked Doors", LegendStyle::ShortDash);
draw_legend_entry_colorblind(ui, "Archways", LegendStyle::DottedLine);
} else {
draw_legend_entry(ui, Color32::from_rgb(70, 120, 160), "Rooms");
draw_legend_entry(ui, Color32::from_rgb(210, 190, 120), "Corridors");
draw_legend_entry(ui, Color32::BLACK, "Walls");
draw_legend_entry(ui, Color32::from_rgb(220, 70, 70), "Doors");
draw_legend_entry(ui, Color32::from_rgb(80, 200, 120), "Locked Doors");
draw_legend_entry(ui, Color32::from_rgb(70, 130, 220), "Archways");
}
}); });
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
@@ -91,7 +106,12 @@ impl eframe::App for DungeonApp {
let canvas = response.rect.shrink(12.0); let canvas = response.rect.shrink(12.0);
let geometry = draw_grid(&painter, canvas, self.settings.cols, self.settings.rows); let geometry = draw_grid(&painter, canvas, self.settings.cols, self.settings.rows);
self.handle_drag(&response, &geometry); self.handle_drag(&response, &geometry);
draw_layout(&painter, &geometry, &self.layout); draw_layout(
&painter,
&geometry,
&self.layout,
self.settings.colorblind_mode,
);
}); });
} }
} }
@@ -356,12 +376,23 @@ fn draw_grid(painter: &egui::Painter, area: egui::Rect, cols: usize, rows: usize
} }
} }
fn draw_layout(painter: &egui::Painter, geometry: &GridGeometry, layout: &DungeonLayout) { fn draw_layout(
painter: &egui::Painter,
geometry: &GridGeometry,
layout: &DungeonLayout,
colorblind_mode: bool,
) {
let room_fill = Color32::from_rgb(70, 120, 160); let room_fill = Color32::from_rgb(70, 120, 160);
let corridor_fill = Color32::from_rgb(210, 190, 120); let corridor_fill = Color32::from_rgb(210, 190, 120);
let outline_stroke = Stroke::new(3.0, Color32::BLACK); let outline_stroke = Stroke::new(3.0, Color32::BLACK);
let mut corridor_cells_set = HashSet::new(); let mut corridor_cells_set = HashSet::new();
let mut corridor_edges_set = HashSet::new(); let mut corridor_edges_set = HashSet::new();
let mut room_cells_set = HashSet::new();
let mut door_edges_set = HashSet::new();
for door in &layout.doors {
door_edges_set.insert(normalized_edge(door.from, door.to));
}
for corridor in &layout.corridors { for corridor in &layout.corridors {
for &cell in &corridor.path { for &cell in &corridor.path {
@@ -374,14 +405,28 @@ fn draw_layout(painter: &egui::Painter, geometry: &GridGeometry, layout: &Dungeo
for &(col, row) in &corridor_cells_set { for &(col, row) in &corridor_cells_set {
painter.rect_filled(cell_rect(geometry, col, row), 0.0, corridor_fill); painter.rect_filled(cell_rect(geometry, col, row), 0.0, corridor_fill);
if colorblind_mode {
painter.circle_filled(
cell_center(geometry, col, row),
geometry.cell_size * 0.14,
Color32::BLACK,
);
}
} }
for &(col, row) in &corridor_cells_set { for &(col, row) in &corridor_cells_set {
let rect = cell_rect(geometry, col, row); let rect = cell_rect(geometry, col, row);
let right = (col + 1, row); let right = (col + 1, row);
let bottom = (col, row + 1); let bottom = (col, row + 1);
let left = col.checked_sub(1).map(|x| (x, row));
let top = row.checked_sub(1).map(|y| (col, y));
if col == 0 || !corridor_cells_set.contains(&(col - 1, row)) { if col == 0
|| (!corridor_cells_set.contains(&(col - 1, row))
&& left
.map(|n| !door_edges_set.contains(&normalized_edge((col, row), n)))
.unwrap_or(true))
{
painter.line_segment( painter.line_segment(
[ [
egui::pos2(rect.left(), rect.top()), egui::pos2(rect.left(), rect.top()),
@@ -390,8 +435,9 @@ fn draw_layout(painter: &egui::Painter, geometry: &GridGeometry, layout: &Dungeo
outline_stroke, outline_stroke,
); );
} }
if !corridor_cells_set.contains(&right) if (!corridor_cells_set.contains(&right)
|| !corridor_edges_set.contains(&normalized_edge((col, row), right)) || !corridor_edges_set.contains(&normalized_edge((col, row), right)))
&& !door_edges_set.contains(&normalized_edge((col, row), right))
{ {
painter.line_segment( painter.line_segment(
[ [
@@ -401,7 +447,12 @@ fn draw_layout(painter: &egui::Painter, geometry: &GridGeometry, layout: &Dungeo
outline_stroke, outline_stroke,
); );
} }
if row == 0 || !corridor_cells_set.contains(&(col, row - 1)) { if row == 0
|| (!corridor_cells_set.contains(&(col, row - 1))
&& top
.map(|n| !door_edges_set.contains(&normalized_edge((col, row), n)))
.unwrap_or(true))
{
painter.line_segment( painter.line_segment(
[ [
egui::pos2(rect.left(), rect.top()), egui::pos2(rect.left(), rect.top()),
@@ -410,8 +461,9 @@ fn draw_layout(painter: &egui::Painter, geometry: &GridGeometry, layout: &Dungeo
outline_stroke, outline_stroke,
); );
} }
if !corridor_cells_set.contains(&bottom) if (!corridor_cells_set.contains(&bottom)
|| !corridor_edges_set.contains(&normalized_edge((col, row), bottom)) || !corridor_edges_set.contains(&normalized_edge((col, row), bottom)))
&& !door_edges_set.contains(&normalized_edge((col, row), bottom))
{ {
painter.line_segment( painter.line_segment(
[ [
@@ -424,6 +476,12 @@ fn draw_layout(painter: &egui::Painter, geometry: &GridGeometry, layout: &Dungeo
} }
for room in &layout.rooms { 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_set.insert((x, y));
}
}
let left = geometry.rect.left() + room.x as f32 * geometry.cell_size; let left = geometry.rect.left() + room.x as f32 * geometry.cell_size;
let top = geometry.rect.top() + room.y as f32 * geometry.cell_size; let top = geometry.rect.top() + room.y as f32 * geometry.cell_size;
let right = left + room.width as f32 * geometry.cell_size; let right = left + room.width as f32 * geometry.cell_size;
@@ -431,23 +489,98 @@ fn draw_layout(painter: &egui::Painter, geometry: &GridGeometry, layout: &Dungeo
let room_rect = egui::Rect::from_min_max(egui::pos2(left, top), egui::pos2(right, bottom)); let room_rect = egui::Rect::from_min_max(egui::pos2(left, top), egui::pos2(right, bottom));
painter.rect_filled(room_rect, 0.0, room_fill); painter.rect_filled(room_rect, 0.0, room_fill);
painter.rect_stroke(room_rect, 0.0, outline_stroke, egui::StrokeKind::Middle); if colorblind_mode {
draw_room_crosshatch(painter, geometry, room, Stroke::new(1.5, Color32::BLACK));
}
}
for &(col, row) in &room_cells_set {
let rect = cell_rect(geometry, col, row);
let right = (col + 1, row);
let bottom = (col, row + 1);
let left = col.checked_sub(1).map(|x| (x, row));
let top = row.checked_sub(1).map(|y| (col, y));
if col == 0
|| (!room_cells_set.contains(&(col - 1, row))
&& left
.map(|n| !door_edges_set.contains(&normalized_edge((col, row), n)))
.unwrap_or(true))
{
painter.line_segment(
[
egui::pos2(rect.left(), rect.top()),
egui::pos2(rect.left(), rect.bottom()),
],
outline_stroke,
);
}
if !room_cells_set.contains(&right)
&& !door_edges_set.contains(&normalized_edge((col, row), right))
{
painter.line_segment(
[
egui::pos2(rect.right(), rect.top()),
egui::pos2(rect.right(), rect.bottom()),
],
outline_stroke,
);
}
if row == 0
|| (!room_cells_set.contains(&(col, row - 1))
&& top
.map(|n| !door_edges_set.contains(&normalized_edge((col, row), n)))
.unwrap_or(true))
{
painter.line_segment(
[
egui::pos2(rect.left(), rect.top()),
egui::pos2(rect.right(), rect.top()),
],
outline_stroke,
);
}
if !room_cells_set.contains(&bottom)
&& !door_edges_set.contains(&normalized_edge((col, row), bottom))
{
painter.line_segment(
[
egui::pos2(rect.left(), rect.bottom()),
egui::pos2(rect.right(), rect.bottom()),
],
outline_stroke,
);
}
} }
for door in &layout.doors { for door in &layout.doors {
let color = if door.archway { let color = if colorblind_mode {
Color32::BLACK
} else if door.archway {
Color32::from_rgb(70, 130, 220) Color32::from_rgb(70, 130, 220)
} else if door.locked { } else if door.locked {
Color32::from_rgb(80, 200, 120) Color32::from_rgb(80, 200, 120)
} else { } else {
Color32::from_rgb(220, 70, 70) Color32::from_rgb(220, 70, 70)
}; };
let style = if colorblind_mode {
if door.archway {
DoorLineStyle::Dotted
} else if door.locked {
DoorLineStyle::ShortDash
} else {
DoorLineStyle::LongDash
}
} else {
DoorLineStyle::Solid
};
draw_door_line( draw_door_line(
painter, painter,
geometry, geometry,
door.from, door.from,
door.to, door.to,
Stroke::new(3.0, color), Stroke::new(3.0, color),
style,
); );
} }
} }
@@ -556,6 +689,7 @@ fn draw_door_line(
a: (usize, usize), a: (usize, usize),
b: (usize, usize), b: (usize, usize),
stroke: Stroke, stroke: Stroke,
style: DoorLineStyle,
) { ) {
if a.0.abs_diff(b.0) + a.1.abs_diff(b.1) != 1 { if a.0.abs_diff(b.0) + a.1.abs_diff(b.1) != 1 {
return; return;
@@ -566,13 +700,108 @@ fn draw_door_line(
let row = a.1; let row = a.1;
let y0 = geometry.rect.top() + row as f32 * geometry.cell_size; let y0 = geometry.rect.top() + row as f32 * geometry.cell_size;
let y1 = y0 + geometry.cell_size; let y1 = y0 + geometry.cell_size;
painter.line_segment([egui::pos2(x, y0), egui::pos2(x, y1)], stroke); draw_styled_line(painter, egui::pos2(x, y0), egui::pos2(x, y1), stroke, style);
} else { } else {
let y = geometry.rect.top() + (a.1.max(b.1) as f32) * geometry.cell_size; let y = geometry.rect.top() + (a.1.max(b.1) as f32) * geometry.cell_size;
let col = a.0; let col = a.0;
let x0 = geometry.rect.left() + col as f32 * geometry.cell_size; let x0 = geometry.rect.left() + col as f32 * geometry.cell_size;
let x1 = x0 + geometry.cell_size; let x1 = x0 + geometry.cell_size;
painter.line_segment([egui::pos2(x0, y), egui::pos2(x1, y)], stroke); draw_styled_line(painter, egui::pos2(x0, y), egui::pos2(x1, y), stroke, style);
}
}
#[derive(Debug, Clone, Copy)]
enum DoorLineStyle {
Solid,
LongDash,
ShortDash,
Dotted,
}
fn draw_styled_line(
painter: &egui::Painter,
from: egui::Pos2,
to: egui::Pos2,
stroke: Stroke,
style: DoorLineStyle,
) {
match style {
DoorLineStyle::Solid => {
painter.line_segment([from, to], stroke);
}
DoorLineStyle::LongDash => draw_dashed_line(painter, from, to, stroke, 14.0, 7.0),
DoorLineStyle::ShortDash => draw_dashed_line(painter, from, to, stroke, 6.0, 4.0),
DoorLineStyle::Dotted => draw_dotted_line(painter, from, to, stroke),
}
}
fn draw_dashed_line(
painter: &egui::Painter,
from: egui::Pos2,
to: egui::Pos2,
stroke: Stroke,
dash_len: f32,
gap_len: f32,
) {
let dx = to.x - from.x;
let dy = to.y - from.y;
let len = (dx * dx + dy * dy).sqrt();
if len <= 0.0 {
return;
}
let ux = dx / len;
let uy = dy / len;
let mut dist = 0.0_f32;
while dist < len {
let seg_start = dist;
let seg_end = (dist + dash_len).min(len);
let p0 = egui::pos2(from.x + ux * seg_start, from.y + uy * seg_start);
let p1 = egui::pos2(from.x + ux * seg_end, from.y + uy * seg_end);
painter.line_segment([p0, p1], stroke);
dist += dash_len + gap_len;
}
}
fn draw_dotted_line(painter: &egui::Painter, from: egui::Pos2, to: egui::Pos2, stroke: Stroke) {
let dx = to.x - from.x;
let dy = to.y - from.y;
let len = (dx * dx + dy * dy).sqrt();
if len <= 0.0 {
return;
}
let ux = dx / len;
let uy = dy / len;
let step = 5.0_f32;
let radius = (stroke.width * 0.35).max(1.0);
let mut dist = 0.0_f32;
while dist <= len {
let p = egui::pos2(from.x + ux * dist, from.y + uy * dist);
painter.circle_filled(p, radius, stroke.color);
dist += step;
}
}
fn cell_center(geometry: &GridGeometry, col: usize, row: usize) -> egui::Pos2 {
egui::pos2(
geometry.rect.left() + (col as f32 + 0.5) * geometry.cell_size,
geometry.rect.top() + (row as f32 + 0.5) * geometry.cell_size,
)
}
fn draw_room_crosshatch(
painter: &egui::Painter,
geometry: &GridGeometry,
room: &layout::Room,
stroke: Stroke,
) {
for x in room.x..(room.x + room.width) {
for y in room.y..(room.y + room.height) {
let rect = cell_rect(geometry, x, y).shrink(2.0);
painter.line_segment([rect.left_top(), rect.right_bottom()], stroke);
painter.line_segment([rect.right_top(), rect.left_bottom()], stroke);
}
} }
} }
@@ -591,3 +820,88 @@ fn draw_legend_entry(ui: &mut egui::Ui, color: Color32, label: &str) {
}); });
ui.add_space(4.0); ui.add_space(4.0);
} }
#[derive(Debug, Clone, Copy)]
enum LegendStyle {
Crosshatch,
Dots,
SolidLine,
LongDash,
ShortDash,
DottedLine,
}
fn draw_legend_entry_colorblind(ui: &mut egui::Ui, label: &str, style: LegendStyle) {
ui.horizontal(|ui| {
let (rect, _resp) = ui.allocate_exact_size(egui::vec2(16.0, 16.0), egui::Sense::hover());
ui.painter().rect_filled(rect, 0.0, Color32::from_gray(220));
ui.painter().rect_stroke(
rect,
0.0,
Stroke::new(1.0, Color32::BLACK),
egui::StrokeKind::Middle,
);
match style {
LegendStyle::Crosshatch => {
ui.painter().line_segment(
[rect.left_top(), rect.right_bottom()],
Stroke::new(1.2, Color32::BLACK),
);
ui.painter().line_segment(
[rect.right_top(), rect.left_bottom()],
Stroke::new(1.2, Color32::BLACK),
);
}
LegendStyle::Dots => {
ui.painter()
.circle_filled(rect.center(), 2.0, Color32::BLACK);
ui.painter().circle_filled(
egui::pos2(rect.center().x - 4.0, rect.center().y),
1.4,
Color32::BLACK,
);
ui.painter().circle_filled(
egui::pos2(rect.center().x + 4.0, rect.center().y),
1.4,
Color32::BLACK,
);
}
LegendStyle::SolidLine => {
ui.painter().line_segment(
[
egui::pos2(rect.left() + 1.0, rect.center().y),
egui::pos2(rect.right() - 1.0, rect.center().y),
],
Stroke::new(2.0, Color32::BLACK),
);
}
LegendStyle::LongDash => draw_dashed_line(
ui.painter(),
egui::pos2(rect.left() + 1.0, rect.center().y),
egui::pos2(rect.right() - 1.0, rect.center().y),
Stroke::new(2.0, Color32::BLACK),
6.0,
3.0,
),
LegendStyle::ShortDash => draw_dashed_line(
ui.painter(),
egui::pos2(rect.left() + 1.0, rect.center().y),
egui::pos2(rect.right() - 1.0, rect.center().y),
Stroke::new(2.0, Color32::BLACK),
3.0,
2.0,
),
LegendStyle::DottedLine => draw_dotted_line(
ui.painter(),
egui::pos2(rect.left() + 1.0, rect.center().y),
egui::pos2(rect.right() - 1.0, rect.center().y),
Stroke::new(2.0, Color32::BLACK),
),
}
ui.add_space(6.0);
ui.label(label);
});
ui.add_space(4.0);
}
+2
View File
@@ -31,6 +31,7 @@ pub struct UiSettings {
pub room_hallway_door_percent: usize, pub room_hallway_door_percent: usize,
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,
active_tab: Tab, active_tab: Tab,
} }
@@ -50,6 +51,7 @@ impl Default for UiSettings {
room_hallway_door_percent: 70, room_hallway_door_percent: 70,
locked_door_percent: 25, locked_door_percent: 25,
allow_middle_corridor_doors: false, allow_middle_corridor_doors: false,
colorblind_mode: false,
active_tab: Tab::Generate, active_tab: Tab::Generate,
} }
} }